La programación asíncrona es adecuada para tareas que incluyen leer y escribir archivos con frecuencia o enviar datos de un servidor a otro. Los programas asíncronos realizan operaciones de E / S de forma no bloqueante, lo que significa que pueden realizar otras tareas mientras esperan que los datos regresen de un cliente en lugar de esperar de forma ociosa, desperdiciando recursos y tiempo.
Python, como muchos otros lenguajes, no es asíncrono por defecto. Afortunadamente, los rápidos cambios en el mundo de las TI nos permiten escribir código asíncrono, incluso utilizando lenguajes que originalmente no estaban destinados a hacerlo. A lo largo de los años, las demandas de velocidad están superando las capacidades de hardware y las compañías de todo el mundo se han unido con el Manifiesto reactivo para tratar este problema.
El comportamiento de no bloqueo de los programas asíncronos puede generar importantes beneficios de rendimiento en el contexto de una aplicación web, lo que ayuda a resolver el problema del desarrollo de aplicaciones reactivas.
Cocinadas en Python 3 hay algunas herramientas poderosas para escribir aplicaciones asíncronas. En este artículo, cubriremos algunas de estas herramientas, especialmente en lo relacionado con el desarrollo web.
Desarrollaremos una aplicación simple reactiva basada en aiohttp para mostrar las coordenadas del cielo relevantes actuales 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 desplegarla en Heroku .

Introducción a Python asíncrono

Para aquellos familiarizados con la escritura del código Python tradicional, hacer el salto al código asíncrono puede ser conceptualmente un poco complicado. El código asíncrono en Python se basa en coroutines , que junto con un bucle de eventos permiten escribir código que puede parecer que está haciendo más de una cosa a la vez.
Se puede considerar a los coroutines como funciones que tienen puntos en el código donde le dan 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é parte del código se ejecuta en un momento dado: es responsable de pausar, reanudar y comunicarse entre las rutinas. Esto significa que partes de diferentes coroutines pueden terminar ejecutándose en un orden diferente al que estaban programados. Esta idea de ejecutar diferentes trozos de código fuera de orden se denomina concurrencia .
Pensar en la concurrencia en el contexto de la realización de HTTPsolicitudes puede ser esclarecedor. Imagina querer hacer muchas solicitudes independientes a un servidor. Por ejemplo, podríamos querer consultar un sitio web para obtener estadísticas sobre todos los deportistas en una temporada determinada.
Nosotros podríamos hacer que cada solicitud secuencialmente. Sin embargo, con cada solicitud, podemos imaginar que nuestro código puede pasar algún tiempo esperando a que se envíe una solicitud al servidor y que la respuesta se envíe de vuelta.
A veces, estas operaciones pueden llevar incluso varios segundos. La aplicación puede experimentar un retraso en la red debido a un gran número 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 llegasen los datos de respuesta? Podríamos hacer muchas solicitudes en rápida sucesión si no tuviéramos que esperar a que cada solicitud individual finalizara antes de pasar a la siguiente en la lista.
Las rutinas con un bucle de eventos nos permiten escribir código que se comporta 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 coroutines para su ejecución, y crear nuevas coroutines (realmente asyncio.Taskobjetos, usando el lenguaje de asyncio ) que solo terminarán de ejecutarse una vez que los coroutines 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 se envía con el lenguaje. Como señala Brett Cannon , Python coroutines constituye una API asíncrona, 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 "detrás de la escena"), como uvloop .
Echemos un vistazo a un fragmento de código que ejecuta dos coroutines al mismo tiempo, cada una imprime 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())  
me@local:~$ 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 el asyncio.sleepcoroutine establece puntos en los que el bucle de eventos puede saltar para ejecutar otro código. Además, le hemos dicho al ciclo de eventos que programe ambas wait_aroundinstancias para la ejecución concurrente con la asyncio.gatherfunción.
asyncio.gathertoma una lista de "aguardables" (es decir, coroutines u asyncio.Taskobjetos) y devuelve un solo asyncio.Taskobjeto que solo finaliza cuando todas sus tareas constitutivas / coroutines están terminadas. Las dos últimas líneas son asynciorepetibles para ejecutar una coroutina determinada hasta que finaliza su ejecución.
Coroutines, a diferencia de las funciones, no comenzará a ejecutarse inmediatamente después de ser invocadas. La awaitpalabra clave es lo que le dice al ciclo de eventos que programe una rutina para la ejecución.
Si eliminamos el awaitfrente asyncio.sleep, el programa finaliza (casi) al instante, ya que no le hemos dicho al bucle de eventos que ejecute realmente la rutina, que en este caso le dice a la rutina que se detenga por un tiempo determinado.
Con una idea de cómo se ve el código asíncrono de Python, pasemos al desarrollo web asíncrono.

Instalando aiohttp

aiohttp es una biblioteca de Python para realizar HTTPsolicitudes asíncronas 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: Hacer solicitudes

Los siguientes ejemplos muestran cómo podemos descargar el contenido HTML del sitio web "example.com" utilizando 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 los await asyncio.sleepque hay que utilizar awaitcon resp.text()el fin de obtener el contenido HTML de la página. Si lo dejamos fuera, la salida de nuestro programa sería algo como lo siguiente:
me@local:~$ python example2_basic_aiohttp_request.py  
<coroutine object ClientResponse.text at 0x7fe64e574ba0>  
  • async withEs un gestor de contexto que trabaja con coroutines 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 verbos HTTP . De la misma manera que session.getestá haciendo una solicitud GET , session.postharía una solicitud POST .
Este ejemplo por sí mismo no ofrece ninguna ventaja de rendimiento sobre la realización de solicitudes HTTP síncronas. La verdadera belleza de aiohttp del lado del cliente reside en realizar múltiples solicitudes simultáneas:
# 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 hacer cada solicitud de forma secuencial, le pedimos asyncioque las haga simultáneamente asycio.gather.

Aplicación Web PlanetTracker

A lo largo de esta sección, tengo la intención de demostrar cómo armar una aplicación que informa 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 adelante mientras trabajo para armar la aplicación, debe hacer lo siguiente, suponiendo que tiene Python 3.6 y pip instalados:
me@local:~$ mkdir planettracker && cd planettracker  
me@local:~/planettracker$ pip install --user pipenv  
me@local:~/planettracker$ pipenv --python=3  

Planetas Efemérides con PyEphem

Las efemérides de un objeto astronómico es su posición actual en el cielo en un lugar y tiempo dados en la Tierra. PyEphem es una biblioteca de Python que permite el cálculo preciso de efemérides.
Se adapta especialmente bien a la tarea en cuestión, ya que tiene objetos astronómicos comunes cocinados en la biblioteca. Primero, instalemos PyEphem :
me@local:~/planettracker$ pipenv install ephem  
Obtener las coordenadas actuales de Marte es tan simple como usar una instancia de la Observerclase para 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 del planeta, configuremos una clase PlanetTrackercon un método que devuelva el azimith y la altitud actuales de un planeta dado, en grados ( PyEphem utiliza de manerapredeterminada 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 obtener 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'}

Server-Side aiohttp: 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 comenzar 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 recibiendo 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 el get_planet_ephmeriscoroutine sea el controlador para una GETruta variable .
Antes de ejecutar esto, instalemos aiohttp con pipenv:
me@local:~/planettracker$ pipenv install aiohttp  
Ahora podemos ejecutar nuestra aplicación:
me@local:~/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 pongo 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"}
Esto es lo mismo que emitir el siguiente comando curl :
me@local:~$ 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 suministrar una URL GET para curl :
me@local:~$ 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 Marte en otras ubicaciones (tenga en cuenta las citas de la URL):
me@local:~$ 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 también:
me@local:~$ 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, curlse supone automáticamente que estamos realizando una solicitud POST.
Antes de continuar, debo tener en cuenta que la web.run_appfunción ejecuta nuestra aplicación de forma bloqueada. ¡Esto definitivamente no es lo que estamos buscando lograr!
Para ejecutarlo al mismo tiempo, 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())
Note la presencia de en loop.run_foreverlugar de la llamada a la 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 , en cuyo punto cerrará el servidor con gracia.

Cliente HTML / JavaScript

aiohttp nos permite servir archivos HTML y JavaScript. No se recomienda el uso de aiohttp para servir activos "estáticos" como CSS y JavaScript, pero a los efectos de esta aplicación, no debería ser un problema.
Agreguemos algunas líneas a nuestro aiohttp_app.pyarchivo para presentar un archivo HTML que haga referencia a un archivo JavaScript:
# aiohttp_app.py
...
@routes.get('/')
async def hello(request):  
    return web.FileResponse("./index.html")


app = web.Application()  
app.add_routes(routes)  
app.router.add_static("/", "./")  
...
El hellocoroutine está configurando una ruta GET 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 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 los archivos HTML, CSS y JS a un directorio separado que se sirve por sí 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 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 comprender qué están haciendo las diferentes partes del script:
  • createPlanetDisplay está creando elementos HTML de forma dinámica y los vincula 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, debemos estar usando HTTPS no HTTP ).
  • getPlanetEphemerisgetPlanetEphemeridesrealizar solicitudes GET al servidor para obtener efemérides para un planeta específico y obtener efemérides para todos los planetas, respectivamente.
  • testPerformancerealiza nsolicitudes al servidor y determina cuánto tiempo lleva.

Manual sobre el despliegue en Heroku

Heroku es un servicio para desplegar fácilmente aplicaciones web. Heroku se encarga de configurar los componentes orientados a la web de una aplicación, como configurar proxies inversos o preocuparse por el equilibrio de carga. Para aplicaciones que manejan pocas solicitudes y un pequeño número de usuarios, Heroku es un excelente servicio de alojamiento gratuito.
El despliegue de aplicaciones Python en Heroku se ha vuelto muy fácil en los últimos años. Básicamente, tenemos que crear dos archivos que enumeren las dependencias de nuestra aplicación y decirle a Heroku cómo ejecutar nuestra aplicación.
Un Pipfile se encarga de lo primero, mientras que un Procfile se encarga de lo último. Un Pipfile se mantiene utilizando pipenv: agregamos a nuestro Pipfile (y Pipfile.lock) cada vez que instalamos una dependencia.
Para ejecutar nuestra aplicación en Heroku, debemos agregar una dependencia más:
me@local:~/planettracker$ pipenv install gunicorn  
Podemos crear nuestro propio Procfile, agregándole la siguiente línea:
web: gunicorn aiohttp_app:app --worker-class aiohttp.GunicornWebWorker  
Básicamente, esto le dice a Heroku que use Gunicornpara ejecutar nuestra aplicación, usando el trabajador web especial aiohttp .
Antes de poder implementarlo en Heroku, deberá comenzar a rastrear la aplicación con Git:
me@local:~/planettracker$ git init  
me@local:~/planettracker$ git add .  
me@local:~/planettracker$ git commit -m "first commit"  
Ahora puede seguir las instrucciones en el 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 el aspecto del desarrollo web asíncrono en Python: sus ventajas y usos. Posteriormente, creamos una aplicación simple reactiva basada en aiohttp que muestra dinámicamente las coordenadas del cielo relevantes actuales de los planetas del Sistema Solar, dadas las coordenadas geográficas del usuario.
Al crear la aplicación, la hemos preparado para su implementación en Heroku.
Como se mencionó anteriormente, puede encontrar tanto el código fuente como la demostración de la aplicación si es necesario.