Python asíncrono para el desarrollo web

P

La programación asincrónica es adecuada para tareas que incluyen leer y escribir archivos con frecuencia o enviar datos de un servidor a otro. Los programas asincrónicos realizan operaciones de E / S sin bloqueo, lo que significa que pueden realizar otras tareas mientras esperan que los datos regresen de un cliente en lugar de esperar sin hacer nada, desperdiciando recursos y tiempo.

Python, como muchos otros lenguajes, adolece de no ser asincrónico por defecto. Afortunadamente, los rápidos cambios en el mundo de las tecnologías de la información nos permiten escribir código asincrónico incluso utilizando lenguajes que originalmente no estaban destinados a hacerlo. A lo largo de los años, las demandas de velocidad superan las capacidades del hardware y las empresas de todo el mundo se han unido con el Manifiesto Reactivo para abordar este problema.

El comportamiento de no bloqueo de los programas asincrónicos puede resultar en importantes beneficios de rendimiento en el contexto de una aplicación web, ayudando a abordar el problema del desarrollo de aplicaciones reactivas.

Cocidas en Python 3 hay algunas herramientas poderosas para escribir aplicaciones asincrónicas. En este artículo, cubriremos algunas de estas herramientas, especialmente en lo que se refiere al desarrollo web.

Desarrollaremos una aplicación reactiva simple basada en aiohttp para mostrar las coordenadas actuales relevantes del cielo de los planetas del Sistema Solar, dadas las coordenadas geográficas del usuario. Puede encontrar la aplicación aquí y el código fuente aquí .

Terminaremos discutiendo cómo preparar la aplicación para implementarla en Heroku .

Introducción a Python asincrónico

Para aquellos familiarizados con la escritura de código Python tradicional, dar el salto al código asincrónico puede ser conceptualmente un poco complicado. El código asincrónico en Python se basa en corrutinas , que junto con un bucle de eventos permiten escribir código que puede parecer que hace más de una cosa a la vez.

Las corrutinas se pueden considerar como funciones que tienen puntos en el código donde devuelven el control del programa al contexto de llamada. Estos puntos de “rendimiento” permiten pausar y reanudar la ejecución de una rutina, además de intercambiar datos entre contextos.

El bucle de eventos decide qué fragmento de código se ejecuta en un momento dado: es responsable de pausar, reanudar y comunicarse entre corrutinas. Esto significa que partes de diferentes corrutinas podrían terminar ejecutándose en un orden diferente al que estaban programadas. Esta idea de ejecutar diferentes fragmentos de código fuera de orden se llama concurrencia .

Pensar en la concurrencia en el contexto de la realización de HTTPsolicitudes puede resultar esclarecedor. Imagínese querer hacer muchas solicitudes independientes a un servidor. Por ejemplo, es posible que deseemos consultar un sitio web para obtener estadísticas sobre todos los jugadores deportivos en una temporada determinada.

Podríamos realizar cada solicitud de forma secuencial. Sin embargo, con cada solicitud, podemos imaginar que nuestro código podría pasar algún tiempo esperando que se envíe una solicitud al servidor y que se envíe la respuesta.

A veces, estas operaciones pueden llevar incluso varios segundos. La aplicación puede experimentar un retraso en la red debido a una gran cantidad de usuarios, o simplemente debido a los límites de velocidad del servidor dado.

¿Qué pasaría si nuestro código pudiera hacer otras cosas mientras espera una respuesta del servidor? Además, ¿qué pasaría si solo volviera a procesar una solicitud determinada una vez que llegaran los datos de respuesta? Podríamos hacer muchas solicitudes en rápida sucesión si no tuviéramos que esperar a que finalice cada solicitud individual antes de pasar a la siguiente en la lista.

Las corrutinas con un bucle de eventos nos permiten escribir código que se comporte exactamente de esta manera.

asyncio

asyncio , parte de la biblioteca estándar de Python, proporciona un bucle de eventos y un conjunto de herramientas para controlarlo. Con asyncio podemos programar corrutinas para su ejecución y crear nuevas corrutinas (realmente asyncio.Taskobjetos, usando el lenguaje de asyncio) que solo terminarán de ejecutarse una vez que las corrutinas constituyentes terminen de ejecutarse.

A diferencia de otros lenguajes de programación asíncronos, Python no nos obliga a usar el bucle de eventos que viene con el lenguaje. Como señala Brett Cannon , las corrutinas de Python constituyen una API asincrónica, con la que podemos utilizar cualquier bucle de eventos. Existen proyectos que implementan un bucle de eventos completamente diferente, como curio , o permiten colocar una política de bucle de eventos diferente para asyncio (la política de bucle de eventos es lo que administra el bucle de eventos “entre bastidores”), como uvloop .

Echemos un vistazo a un fragmento de código que ejecuta dos corrutinas al mismo tiempo, cada una imprimiendo un mensaje después de un segundo:

# example1.py
import asyncio

async def wait_around(n, name):
    for i in range(n):
        print(f"{name}: iteration {i}")
        await asyncio.sleep(1.0)

async def main():
    await asyncio.gather(*[
        wait_around(2, "coroutine 0"), wait_around(5, "coroutine 1")
    ])

loop = asyncio.get_event_loop()
loop.run_until_complete(main())
[email protected]:~$ time python example1.py
coroutine 1: iteration 0
coroutine 0: iteration 0
coroutine 1: iteration 1
coroutine 0: iteration 1
coroutine 1: iteration 2
coroutine 1: iteration 3
coroutine 1: iteration 4

real    0m5.138s
user    0m0.111s
sys     0m0.019s

Este código se ejecuta en aproximadamente 5 segundos, ya que la asyncio.sleepcorrutina establece puntos en los que el bucle de eventos puede saltar para ejecutar otro código. Además, le hemos dicho al bucle de eventos que programe ambas wait_aroundinstancias para la ejecución simultánea con la asyncio.gatherfunción.

asyncio.gathertoma una lista de “esperables” (es decir, corrutinas u asyncio.Taskobjetos) y devuelve un único asyncio.Taskobjeto que solo termina cuando todas sus tareas / corrutinas constituyentes están terminadas. Las dos últimas líneas son asyncioestándar para ejecutar una corrutina determinada hasta que finalice su ejecución.

Las corrutinas, a diferencia de las funciones, no comenzarán a ejecutarse inmediatamente después de ser invocadas. La awaitpalabra clave es lo que le dice al bucle de eventos que programe una corrutina para su ejecución.

Si eliminamos el awaitfrente de asyncio.sleep, el programa finaliza (casi) instantáneamente, ya que no le hemos dicho al bucle de eventos que ejecute la corrutina, que en este caso le dice a la corrutina que haga una pausa durante un período de tiempo determinado.

Con una comprensión de cómo se ve el código Python asincrónico, pasemos al desarrollo web asincrónico.

Instalación de aiohttp

aiohttp es una biblioteca de Python para realizar HTTPsolicitudes asincrónicas . Además, proporciona un marco para armar la parte del servidor de una aplicación web. Usando Python 3.5+ y pip, podemos instalar aiohttp:

pip install --user aiohttp

Lado del cliente: realizar solicitudes

Los siguientes ejemplos muestran cómo podemos descargar el contenido HTML del sitio web “example.com” usando aiohttp:

# example2_basic_aiohttp_request.py
import asyncio
import aiohttp

async def make_request():
    url = "https://example.com"
    print(f"making request to {url}")
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as resp:
            if resp.status == 200:
                print(await resp.text())

loop = asyncio.get_event_loop()
loop.run_until_complete(make_request())

Algunas cosas para enfatizar:

  • Al igual que con await asyncio.sleep, debemos usar awaitcon resp.text()para obtener el contenido HTML de la página. Si lo dejamos fuera, la salida de nuestro programa sería algo como lo siguiente:
[email protected]:~$ python example2_basic_aiohttp_request.py
<coroutine object ClientResponse.text at 0x7fe64e574ba0>
  • async withes un administrador de contexto que trabaja con corrutinas en lugar de funciones. En ambos casos en los que se usa, podemos imaginar que internamente, aiohttp está cerrando las conexiones a los servidores o liberando recursos.
  • aiohttp.ClientSessiontiene métodos que corresponden a los verbos HTTP. De la misma
    forma que session.getestá realizando una solicitud GET , session.postharía una solicitud POST .

Este ejemplo por sí solo no ofrece ninguna ventaja de rendimiento sobre la realización de solicitudes HTTP sincrónicas. La verdadera belleza de aiohttp del lado del cliente radica en realizar múltiples solicitudes concurrentes:

# example3_multiple_aiohttp_request.py
import asyncio
import aiohttp

async def make_request(session, req_n):
    url = "https://example.com"
    print(f"making request {req_n} to {url}")
    async with session.get(url) as resp:
        if resp.status == 200:
            await resp.text()

async def main():
    n_requests = 100
    async with aiohttp.ClientSession() as session:
        await asyncio.gather(
            *[make_request(session, i) for i in range(n_requests)]
        )

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

En lugar de realizar cada solicitud de forma secuencial, pedimos asyncioque las hagamos al mismo tiempo, con asycio.gather.

Aplicación web PlanetTracker

A lo largo de esta sección, pretendo demostrar cómo armar una aplicación que informe las coordenadas actuales de los planetas en el cielo en la ubicación del usuario (efemérides).

El usuario proporciona su ubicación con la API de geolocalización web , que hace el trabajo por nosotros.

Terminaré mostrando cómo configurar un Procfile para implementar la aplicación en Heroku . Si planea seguir mientras trabajo en la creación de la aplicación, debe hacer lo siguiente, asumiendo que tiene Python 3.6 y pip instalados:

[email protected]:~$ mkdir planettracker && cd planettracker
[email protected]:~/planettracker$ pip install --user pipenv
[email protected]:~/planettracker$ pipenv --python=3

Planeta Efemérides con PyEphem

La efemérides de un objeto astronómico es su posición actual en el cielo en un lugar y un momento determinados en la Tierra. PyEphem es una biblioteca de Python que permite calcular con precisión las efemérides.

Es especialmente adecuado para la tarea en cuestión, ya que tiene objetos astronómicos comunes cocinados en la biblioteca. Primero, instalemos PyEphem:

[email protected]:~/planettracker$ pipenv install ephem

Obtener las coordenadas actuales de Marte es tan simple como usar una instancia de la Observerclase en computesus coordenadas:

import ephem
import math
convert = math.pi / 180.
mars = ephem.Mars()
greenwich = ephem.Observer()
greenwich.lat = "51.4769"
greenwich.lon = "-0.0005"
mars.compute(observer)
az_deg, alt_deg = mars.az*convert, mars.alt*convert
print(f"Mars' current azimuth and elevation: {az_deg:.2f} {alt_deg:.2f}")

Para facilitar la obtención de efemérides de planetas, configuremos una clase PlanetTrackercon un método que devuelva el azimit y la altitud actuales de un planeta dado, en grados (PyEphem usa por defecto radianes, no grados, para representar ángulos internamente):

# planet_tracker.py
import math
import ephem

class PlanetTracker(ephem.Observer):

    def __init__(self):
        super(PlanetTracker, self).__init__()
        self.planets = {
            "mercury": ephem.Mercury(),
            "venus": ephem.Venus(),
            "mars": ephem.Mars(),
            "jupiter": ephem.Jupiter(),
            "saturn": ephem.Saturn(),
            "uranus": ephem.Uranus(),
            "neptune": ephem.Neptune()
        }

    def calc_planet(self, planet_name, when=None):
        convert = 180./math.pi
        if when is None:
            when = ephem.now()

        self.date = when
        if planet_name in self.planets:
            planet = self.planets[planet_name]
            planet.compute(self)
            return {
                "az": float(planet.az)*convert,
                "alt": float(planet.alt)*convert,
                "name": planet_name
            }
        else:
            raise KeyError(f"Couldn't find {planet_name} in planets dict")

Ahora podemos conseguir cualquiera de los otros siete planetas del sistema solar con bastante facilidad:

from planet_tracker import PlanetTracker
tracker = PlanetTracker()
tracker.lat = "51.4769"
tracker.lon = "-0.0005"
tracker.calc_planet("mars")

Ejecutar este fragmento de código produciría:

{'az': 92.90019644871396, 'alt': -23.146670983905302, 'name': 'mars'}

Aiohttp del lado del servidor: rutas HTTP

Dada cierta latitud y longitud, podemos obtener fácilmente las efemérides actuales de un planeta, en grados. Ahora configuremos una ruta aiohttp para permitir que un cliente obtenga las efemérides de un planeta dada la geolocalización del usuario.

Antes de que podamos empezar a escribir código, tenemos que pensar qué verbos HTTP queremos asociar con cada una de estas tareas. Tiene sentido usar POST para la primera tarea, ya que estamos configurando las coordenadas geográficas del observador. Dado que estamos obteniendo efemérides, tiene sentido usar GET para la segunda tarea:

# aiohttp_app.py
from aiohttp import web

from planet_tracker import PlanetTracker


@routes.get("/planets/{name}")
async def get_planet_ephmeris(request):
    planet_name = request.match_info['name']
    data = request.query
    try:
        geo_location_data = {
            "lon": str(data["lon"]),
            "lat": str(data["lat"]),
            "elevation": float(data["elevation"])
        }
    except KeyError as err:
        # default to Greenwich Observatory
        geo_location_data = {
            "lon": "-0.0005",
            "lat": "51.4769",
            "elevation": 0.0,
        }
    print(f"get_planet_ephmeris: {planet_name}, {geo_location_data}")
    tracker = PlanetTracker()
    tracker.lon = geo_location_data["lon"]
    tracker.lat = geo_location_data["lat"]
    tracker.elevation = geo_location_data["elevation"]
    planet_data = tracker.calc_planet(planet_name)
    return web.json_response(planet_data)


app = web.Application()
app.add_routes(routes)

web.run_app(app, host="localhost", port=8000)

Aquí, el route.getdecorador indica que queremos que la get_planet_ephmeriscorrutina sea el controlador de una GETruta variable .

Antes de ejecutar esto, instalemos aiohttp con pipenv:

[email protected]:~/planettracker$ pipenv install aiohttp

Ahora podemos ejecutar nuestra aplicación:

[email protected]:~/planettracker$ pipenv run python aiohttp_app.py

Cuando ejecutamos esto, podemos apuntar nuestro navegador a nuestras diferentes rutas para ver los datos que devuelve nuestro servidor. Si coloco localhost:8000/planets/marsen la barra de direcciones de mi navegador, debería ver una respuesta como la siguiente:

{"az": 98.72414165963292, "alt": -18.720718647020792, "name": "mars"}

Es lo mismo que emitir el siguiente comando curl:

[email protected]:~$ curl localhost:8000/planets/mars
{"az": 98.72414165963292, "alt": -18.720718647020792, "name": "mars"}

Si no está familiarizado con curl , es una herramienta de línea de comandos conveniente para, entre otras cosas, probar sus rutas HTTP.

Podemos proporcionar una URL GET para curl:

[email protected]:~$ curl localhost:8000/planets/mars
{"az": 98.72414165963292, "alt": -18.720718647020792, "name": "mars"}

Esto nos da las efemérides de Marte en el Observatorio de Greenwich en el Reino Unido.

Podemos codificar las coordenadas en la URL de la GETsolicitud para que podamos obtener las efemérides de Mars en otras ubicaciones (tenga en cuenta las comillas alrededor de la URL):

[email protected]:~$ curl "localhost:8000/planets/mars?lon=145.051&lat=-39.754&elevation=0"
{"az": 102.30273048280189, "alt": 11.690380174890928, "name": "mars"

curl también se puede utilizar para realizar solicitudes POST:

[email protected]:~$ curl --header "Content-Type: application/x-www-form-urlencoded" --data "lat=48.93&lon=2.45&elevation=0" localhost:8000/geo_location
{"lon": "2.45", "lat": "48.93", "elevation": 0.0}

Tenga en cuenta que al proporcionar el --datacampo, curlautomáticamente se asume que estamos realizando una solicitud POST.

Antes de continuar, debo tener en cuenta que la web.run_appfunción ejecuta nuestra aplicación de manera de bloqueo. ¡Esto definitivamente no es lo que estamos buscando lograr!

Para ejecutarlo simultáneamente, tenemos que agregar un poco más de código:

# aiohttp_app.py
import asyncio
...

# web.run_app(app)

async def start_app():
    runner = web.AppRunner(app)
    await runner.setup()
    site = web.TCPSite(
        runner, parsed.host, parsed.port)
    await site.start()
    print(f"Serving up app on {parsed.host}:{parsed.port}")
    return runner, site

loop = asyncio.get_event_loop()
runner, site = loop.run_until_complete(start_async_app())
try:
    loop.run_forever()
except KeyboardInterrupt as err:
    loop.run_until_complete(runner.cleanup())

Tenga en cuenta la presencia de en loop.run_foreverlugar de la llamada a loop.run_until_completeque vimos anteriormente. En lugar de ejecutar un número determinado de corrutinas, queremos que nuestro programa inicie un servidor que manejará las solicitudes hasta que ctrl+csalgamos con , momento en el que cerrará el servidor de manera elegante.

Cliente HTML / JavaScript

aiohttp nos permite servir archivos HTML y JavaScript. Se desaconseja el uso de aiohttp para entregar activos “estáticos” como CSS y JavaScript, pero para los propósitos de esta aplicación, no debería ser un problema.

Agreguemos algunas líneas a nuestro aiohttp_app.pyarchivo para entregar un archivo HTML que hace referencia a un archivo JavaScript:

# aiohttp_app.py
...
@routes.get("https://Pharos.sh.com/")
async def hello(request):
    return web.FileResponse("./index.html")


app = web.Application()
app.add_routes(routes)
app.router.add_static("/", "./")
...

La hellocorrutina está configurando una ruta GET en localhost:8000/que sirve el contenido de index.html, ubicado en el mismo directorio desde el que ejecutamos nuestro servidor.

La app.router.add_staticlínea está configurando una ruta en localhost:8000/para servir archivos en el mismo directorio desde el que ejecutamos nuestro servidor. Esto significa que nuestro navegador podrá encontrar el archivo JavaScript al que hacemos referencia index.html.

Nota : En producción, tiene sentido mover archivos HTML, CSS y JS a un directorio separado que se sirve solo. Esto hace que el usuario curioso no pueda acceder a nuestro código de servidor.

El archivo HTML es bastante simple:

<!DOCTYPE html>
<html lang='en'>

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Planet Tracker</title>
</head>
<body>
    <div id="app">
        <label id="lon">Longitude: <input type="text"/></label><br/>
        <label id="lat">Latitude: <input type="text"/></label><br/>
        <label id="elevation">Elevation: <input type="text"/></label><br/>
    </div>
    <script src="/app.js"></script>
</body>

Sin embargo, el archivo JavaScript es un poco más complicado:

var App = function() {

    this.planetNames = [
        "mercury",
        "venus",
        "mars",
        "jupiter",
        "saturn",
        "uranus",
        "neptune"
    ]

    this.geoLocationIds = [
        "lon",
        "lat",
        "elevation"
    ]

    this.keyUpInterval = 500
    this.keyUpTimer = null
    this.planetDisplayCreated = false
    this.updateInterval = 2000 // update very second and a half
    this.updateTimer = null
    this.geoLocation = null

    this.init = function() {
        this.getGeoLocation().then((position) => {
            var coords = this.processCoordinates(position)
            this.geoLocation = coords
            this.initGeoLocationDisplay()
            this.updateGeoLocationDisplay()
            return this.getPlanetEphemerides()
        }).then((planetData) => {
            this.createPlanetDisplay()
            this.updatePlanetDisplay(planetData)
        }).then(() => {
            return this.initUpdateTimer()
        })
    }

    this.update = function() {
        if (this.planetDisplayCreated) {
            this.getPlanetEphemerides().then((planetData) => {
                this.updatePlanetDisplay(planetData)
            })
        }
    }

    this.get = function(url, data) {
        var request = new XMLHttpRequest()
        if (data !== undefined) {
            url += `?${data}`
        }
        // console.log(`get: ${url}`)
        request.open("GET", url, true)
        return new Promise((resolve, reject) => {
            request.send()
            request.onreadystatechange = function(){
                if (this.readyState === XMLHttpRequest.DONE && this.status === 200) {
                    resolve(this)
                }
            }
            request.onerror = reject
        })
    }

    this.processCoordinates = function(position) {
        var coordMap = {
            'longitude': 'lon',
            'latitude': 'lat',
            'altitude': 'elevation'
        }
        var coords = Object.keys(coordMap).reduce((obj, name) => {
            var coord = position.coords[name]
            if (coord === null || isNaN(coord)) {
                coord = 0.0
            }
            obj[coordMap[name]] = coord
            return obj
        }, {})
        return coords
    }

    this.coordDataUrl = function (coords) {
        postUrl = Object.keys(coords).map((c) => {
            return `${c}=${coords[c]}`
        })
        return postUrl
    }

    this.getGeoLocation = function() {
        return new Promise((resolve, reject) => {
            navigator.geolocation.getCurrentPosition(resolve)
        })
    }

    this.getPlanetEphemeris = function(planetName) {
        var postUrlArr = this.coordDataUrl(this.geoLocation)
        return this.get(`/planets/${planetName}`, postUrlArr.join("&")).then((req) => {
            return JSON.parse(req.response)
        })
    }

    this.getPlanetEphemerides = function() {
        return Promise.all(
            this.planetNames.map((name) => {
                return this.getPlanetEphemeris(name)
            })
        )
    }

    this.createPlanetDisplay = function() {
        var div = document.getElementById("app")
        var table = document.createElement("table")
        var header = document.createElement("tr")
        var headerNames = ["Name", "Azimuth", "Altitude"]
        headerNames.forEach((headerName) => {
            var headerElement = document.createElement("th")
            headerElement.textContent = headerName
            header.appendChild(headerElement)
        })
        table.appendChild(header)
        this.planetNames.forEach((name) => {
            var planetRow = document.createElement("tr")
            headerNames.forEach((headerName) => {
                planetRow.appendChild(
                    document.createElement("td")
                )
            })
            planetRow.setAttribute("id", name)
            table.appendChild(planetRow)
        })
        div.appendChild(table)
        this.planetDisplayCreated = true
    }

    this.updatePlanetDisplay = function(planetData) {
        planetData.forEach((d) => {
            var content = [d.name, d.az, d.alt]
            var planetRow = document.getElementById(d.name)
            planetRow.childNodes.forEach((node, idx) => {
                var contentFloat = parseFloat(content[idx])
                if (isNaN(contentFloat)) {
                    node.textContent = content[idx]
                } else {
                    node.textContent = contentFloat.toFixed(2)
                }
            })
        })
    }

    this.initGeoLocationDisplay = function() {
        this.geoLocationIds.forEach((id) => {
            var node = document.getElementById(id)
            node.childNodes[1].onkeyup = this.onGeoLocationKeyUp()
        })
        var appNode = document.getElementById("app")
        var resetLocationButton = document.createElement("button")
        resetLocationButton.setAttribute("id", "reset-location")
        resetLocationButton.onclick = this.onResetLocationClick()
        resetLocationButton.textContent = "Reset Geo Location"
        appNode.appendChild(resetLocationButton)
    }

    this.updateGeoLocationDisplay = function() {
        Object.keys(this.geoLocation).forEach((id) => {
            var node = document.getElementById(id)
            node.childNodes[1].value = parseFloat(
                this.geoLocation[id]
            ).toFixed(2)
        })
    }

    this.getDisplayedGeoLocation = function() {
        var displayedGeoLocation = this.geoLocationIds.reduce((val, id) => {
            var node = document.getElementById(id)
            var nodeVal = parseFloat(node.childNodes[1].value)
            val[id] = nodeVal
            if (isNaN(nodeVal)) {
                val.valid = false
            }
            return val
        }, {valid: true})
        return displayedGeoLocation
    }

    this.onGeoLocationKeyUp = function() {
        return (evt) => {
            // console.log(evt.key, evt.code)
            var currentTime = new Date()
            if (this.keyUpTimer !== null){
                clearTimeout(this.keyUpTimer)
            }
            this.keyUpTimer = setTimeout(() => {
                var displayedGeoLocation = this.getDisplayedGeoLocation()
                if (displayedGeoLocation.valid) {
                    delete displayedGeoLocation.valid
                    this.geoLocation = displayedGeoLocation
                    console.log("Using user supplied geo location")
                }
            }, this.keyUpInterval)
        }
    }

    this.onResetLocationClick = function() {
        return (evt) => {
            console.log("Geo location reset clicked")
            this.getGeoLocation().then((coords) => {
                this.geoLocation = this.processCoordinates(coords)
                this.updateGeoLocationDisplay()
            })
        }
    }

    this.initUpdateTimer = function () {
        if (this.updateTimer !== null) {
            clearInterval(this.updateTimer)
        }
        this.updateTimer = setInterval(
            this.update.bind(this),
            this.updateInterval
        )
        return this.updateTimer
    }

    this.testPerformance = function(n) {
        var t0 = performance.now()
        var promises = []
        for (var i=0; i<n; i++) {
            promises.push(this.getPlanetEphemeris("mars"))
        }
        Promise.all(promises).then(() => {
            var delta = (performance.now() - t0)/1000
            console.log(`Took ${delta.toFixed(4)} seconds to do ${n} requests`)
        })
    }
}

var app
document.addEventListener("DOMContentLoaded", (evt) => {
    app = new App()
    app.init()
})

Esta aplicación se actualizará periódicamente (cada 2 segundos) y mostrará las efemérides del planeta. Podemos proporcionar nuestras propias coordenadas geográficas o dejar que la API de geolocalización web determine nuestra ubicación actual. La aplicación actualiza la geolocalización si el usuario deja de escribir durante medio segundo o más.

Si bien este no es un tutorial de JavaScript, creo que es útil para comprender qué están haciendo las diferentes partes del script:

  • createPlanetDisplay está creando dinámicamente elementos HTML y vinculándolos al Modelo de objetos de documento (DOM)
  • updatePlanetDisplay toma los datos recibidos del servidor y llena los elementos creados por createPlanetDisplay
  • getrealiza una solicitud GET al servidor. El objeto XMLHttpRequest permite que esto se haga sin volver a cargar la página.
  • postrealiza una solicitud POST al servidor. Al igual que con getesto se hace sin recargar la página.
  • getGeoLocationutiliza la API de geolocalización web para obtener las coordenadas geográficas actuales del usuario. Esto debe cumplirse “en un contexto seguro” (es decir, que debe utilizar HTTPSno HTTP).
  • getPlanetEphemerisy getPlanetEphemeridesrealizar solicitudes GET al servidor para obtener efemérides para un planeta específico y para obtener efemérides para todos los planetas, respectivamente.
  • testPerformancerealiza nsolicitudes al servidor y determina cuánto tiempo tarda.

Introducción a la implementación en Heroku

Heroku es un servicio para implementar aplicaciones web fácilmente. Heroku se encarga de configurar los componentes web de una aplicación, como configurar proxies inversos o preocuparse por el equilibrio de carga. Para aplicaciones que manejan pocas solicitudes y una pequeña cantidad de usuarios, Heroku es un excelente servicio de alojamiento gratuito.

La implementación de aplicaciones Python en Heroku se ha vuelto muy fácil en los últimos años. En esencia, tenemos que crear dos archivos que enumeren las dependencias de nuestra aplicación y le digan a Heroku cómo ejecutar nuestra aplicación.

Un Pipfile se encarga del primero, mientras que un Procfile se encarga del segundo. Un Pipfile se mantiene usando pipenv– agregamos a nuestro Pipfile (y Pipfile.lock) cada vez que instalamos una dependencia.

Para ejecutar nuestra aplicación en Heroku, tenemos que agregar una dependencia más:

[email protected]:~/planettracker$ pipenv install gunicorn

Podemos crear nuestro propio Procfile, agregando la siguiente línea:

web: gunicorn aiohttp_app:app --worker-class aiohttp.GunicornWebWorker

Básicamente, esto le dice a Heroku que use Gunicorn para ejecutar nuestra aplicación, usando el trabajador web especial aiohttp.

Antes de poder implementar en Heroku, deberá comenzar a rastrear la aplicación con Git:

[email protected]:~/planettracker$ git init
[email protected]:~/planettracker$ git add .
[email protected]:~/planettracker$ git commit -m "first commit"

Ahora puede seguir las instrucciones del centro de desarrollo de Heroku aquí para implementar su aplicación. Tenga en cuenta que puede omitir el paso “Preparar la aplicación” de este tutorial, ya que ya tiene una aplicación con seguimiento de git.

Una vez que se implemente su aplicación, puede navegar a la URL de Heroku elegida en su navegador y ver la aplicación, que se verá así:

Conclusión

En este artículo, nos sumergimos en cómo se ve el desarrollo web asincrónico en Python: sus ventajas y usos. Posteriormente, creamos una aplicación reactiva simple basada en aiohttp que muestra dinámicamente las coordenadas actuales relevantes del cielo de los planetas del Sistema Solar, dadas las coordenadas geográficas del usuario.

Después de crear la aplicación, la preparamos para implementarla en Heroku.

Como se mencionó anteriormente, puede encontrar tanto el código fuente como la demostración de la aplicación si es necesario.

 

About the author

Ramiro de la Vega

Bienvenido a Pharos.sh

Soy Ramiro de la Vega, Estadounidense con raíces Españolas. Empecé a programar hace casi 20 años cuando era muy jovencito.

Espero que en mi web encuentres la inspiración y ayuda que necesitas para adentrarte en el fantástico mundo de la programación y conseguir tus objetivos por difíciles que sean.

Add comment

Sobre mi

Últimos Post

Etiquetas

Esta web utiliza cookies propias para su correcto funcionamiento. Al hacer clic en el botón Aceptar, aceptas el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Más información
Privacidad