Python as铆ncrono para el desarrollo web

    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.

     

    Etiquetas:

    Deja una respuesta

    Tu direcci贸n de correo electr贸nico no ser谩 publicada. Los campos obligatorios est谩n marcados con *