Рубрики
Без рубрики

Асинхронный Python для веб-разработки

Автор оригинала: Dean Shaff.

Асинхронный Python для веб-разработки

Асинхронное программирование хорошо подходит для задач, которые включают в себя частое чтение и запись файлов или отправку данных туда и обратно с сервера. Асинхронные программы выполняют операции ввода-вывода неблокирующим образом, что означает, что они могут выполнять другие задачи, ожидая возвращения данных от клиента, а не ждать праздно, тратя ресурсы и время.

Python, как и многие другие языки, страдает от того, что по умолчанию не является асинхронным. К счастью, быстрые изменения в мире ИТ позволяют нам писать асинхронный код даже с использованием языков, которые изначально не предназначались для этого. На протяжении многих лет требования к скорости превышают аппаратные возможности, и компании по всему миру объединились с Реактивным манифестом , чтобы решить эту проблему.

Неблокирующее поведение асинхронных программ может привести к значительному повышению производительности в контексте веб-приложения, помогая решить проблему разработки реактивных приложений.

В Python 3 есть несколько мощных инструментов для написания асинхронных приложений. В этой статье мы рассмотрим некоторые из этих инструментов, особенно в том, что касается веб-разработки.

Мы будем разрабатывать простое реактивное aiohttp приложение для отображения текущих соответствующих координат неба планет Солнечной системы с учетом географических координат пользователя. Вы можете найти приложение здесь , а исходный код здесь .

В конце мы обсудим, как подготовить приложение к развертыванию в Хероку .

Введение в асинхронный Python

Для тех, кто знаком с написанием традиционного кода Python, переход к асинхронному коду может быть концептуально немного сложным. Асинхронный код в Python опирается на сопрограммы , которые в сочетании с циклом событий позволяют писать код, который, по-видимому, делает несколько вещей одновременно.

Сопрограммы можно рассматривать как функции, имеющие точки в коде, где они возвращают управление программой вызывающему контексту. Эти точки “выхода” позволяют приостанавливать и возобновлять выполнение сопрограммы, а также обмениваться данными между контекстами.

Цикл событий определяет, какой фрагмент кода выполняется в данный момент – он отвечает за приостановку, возобновление и обмен данными между сопрограммами. Это означает, что части различных сопрограмм могут в конечном итоге выполняться в порядке, отличном от того, в котором они были запланированы. Эта идея запуска различных фрагментов кода не по порядку называется параллелизм .

Размышления о параллелизме в контексте создания HTTP запросов могут быть проясняющими. Представьте себе, что вы хотите сделать много независимых запросов к серверу. Например, мы можем запросить веб-сайт, чтобы получить статистику обо всех спортивных игроках в данном сезоне.

Мы могли бы сделать каждый запрос последовательно. Однако с каждым запросом мы можем себе представить, что код out может потратить некоторое время на ожидание запроса, который будет доставлен на сервер, и ответа, который будет отправлен обратно.

Иногда эти операции могут занять даже несколько секунд. Приложение может испытывать сетевое отставание из-за большого количества пользователей или просто из-за ограничений скорости данного сервера.

Что, если наш код может делать другие вещи, ожидая ответа от сервера? Более того, что, если он вернется к обработке данного запроса только после получения данных ответа? Мы могли бы сделать много запросов в быстрой последовательности, если бы нам не приходилось ждать окончания каждого отдельного запроса, прежде чем перейти к следующему в списке.

Сопрограммы с циклом событий позволяют нам писать код, который ведет себя именно так.

asyncio

asyncio , часть стандартной библиотеки Python, предоставляет цикл событий и набор инструментов для управления им. С помощью asyncio мы можем планировать выполнение сопрограмм и создавать новые сопрограммы (действительно asyncio.Task objects, используя язык asyncio ), который завершит выполнение только после завершения выполнения составных сопрограмм.

В отличие от других асинхронных языков программирования, Python не заставляет нас использовать цикл событий, который поставляется вместе с языком. Как указывает Бретт Кэннон , сопрограммы Python представляют собой асинхронный API, с помощью которого мы можем использовать любой цикл событий. Существуют проекты, которые реализуют совершенно другой цикл событий , например curio , или позволяют отбрасывать другую политику цикла событий для asyncio (политика цикла событий-это то, что управляет циклом событий “за кулисами”), например uvloop .

Давайте посмотрим на фрагмент кода, который одновременно запускает две сопрограммы, каждая из которых печатает сообщение через одну секунду:

# 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

Этот код выполняется примерно за 5 секунд, так как сопрограмма asyncio.sleep устанавливает точки, в которых цикл событий может перейти к выполнению другого кода. Более того, мы сказали циклу событий запланировать оба экземпляра wait_around для одновременного выполнения с функцией asyncio.gather .

asyncio.gather принимает список “доступных” (т. е. сопрограмм или asyncio.Task objects) и возвращает один asyncio.Task объект, который завершается только тогда, когда все его составные задачи/сопрограммы завершены. Последние две строки являются asyncio шаблоном для запуска данной сопрограммы до ее завершения.

Сопрограммы, в отличие от функций, не начинают выполняться сразу после их вызова. Ключевое слово await указывает циклу событий запланировать выполнение сопрограммы.

Если мы уберем await перед asyncio.sleep , программа завершится (почти) мгновенно, так как мы не сказали циклу событий фактически выполнить сопрограмму, которая в данном случае говорит сопрограмме приостановиться на заданное количество времени.

Имея представление о том, как выглядит асинхронный код Python, давайте перейдем к асинхронной веб-разработке.

Установка aiohttp

aiohttp – это библиотека Python для выполнения асинхронных HTTP запросов. Кроме того, он обеспечивает основу для объединения серверной части веб-приложения. Используя Python 3.5+ и pip, мы можем установить aiohttp :

pip install --user aiohttp

Клиентская сторона: Выполнение Запросов

Следующие примеры показывают, как мы можем загрузить HTML-содержимое “example.com” использование веб-сайта 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())

Несколько вещей, которые следует подчеркнуть:

  • Очень похоже на await asyncio.sleep мы должны использовать await with resp.text() для того, чтобы получить HTML-содержимое страницы. Если бы мы оставили это, результат нашей программы был бы примерно следующим:
[email protected]:~$ python example2_basic_aiohttp_request.py

  • async with – это контекстный менеджер, который работает с сопрограммами вместо функций. В обоих случаях, когда он используется, мы можем представить, что внутренне aiohttp закрывает соединения с серверами или иным образом освобождает ресурсы.

  • aiohttp.ClientSession имеет методы, соответствующие глаголам HTTP . Точно так же, как session.get делает GET запрос, session.post сделает POST запрос.

Этот пример сам по себе не дает никакого преимущества в производительности по сравнению с синхронными HTTP-запросами. Настоящая красота клиентской части aiohttp заключается в создании нескольких одновременных запросов:

# 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())

Вместо того чтобы делать каждый запрос последовательно, мы просим asyncio делать их одновременно с asyncio.gather .

Веб-приложение Planet Tracker

В этом разделе я намерен продемонстрировать, как собрать приложение, которое сообщает текущие координаты планет на небе в местоположении пользователя (эфемериды).

Пользователь предоставляет свое местоположение с помощью web Geolocation API , который делает всю работу за нас.

В конце концов я покажу, как настроить профиль для развертывания приложения на Хероку . Если вы планируете следить за тем, как я работаю над созданием приложения, вам следует сделать следующее, предполагая, что у вас установлены Python 3.6 и pip:

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

Планета Эфемерид с Пиефемом

Эфемерида астрономического объекта-это его текущее положение в небе в заданном месте и времени на Земле. PyEphem – это библиотека Python, которая позволяет точно вычислять эфемериды.

Он особенно хорошо подходит для выполнения поставленной задачи, так как имеет общие астрономические объекты, приготовленные в библиотеке. Во-первых, давайте установим Пиефем :

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

Получить текущие координаты Марса так же просто, как использовать экземпляр класса Observer для вычисления его координат:

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}")

Чтобы упростить получение эфемерид планет, давайте создадим класс PlanetTracker с методом, который возвращает текущий азимут и высоту данной планеты в градусах ( PyEphem по умолчанию использует радианы, а не градусы, для представления углов внутри):

# 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")

Теперь мы можем получить любую из семи других планет Солнечной системы довольно легко:

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

Запуск этого фрагмента кода приведет к:

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

Серверный aiohttp: HTTP-маршруты

Учитывая некоторую широту и долготу, мы можем легко получить текущую эфемериду планеты в градусах. Теперь давайте настроим маршрут aiohttp , чтобы клиент мог получить эфемериды планеты с учетом геолокации пользователя.

Прежде чем мы начнем писать код, мы должны подумать о том, какие HTTP глаголы мы хотим связать с каждой из этих задач. Имеет смысл использовать POST для первой задачи, так как мы устанавливаем географические координаты наблюдателя. Учитывая, что мы получаем эфемериды, имеет смысл использовать GET для второй задачи:

# 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)

Здесь декоратор route.get указывает, что мы хотим, чтобы сопрограмма get_planet_ephemeris была обработчиком переменной GET route.

Прежде чем мы запустим это, давайте установим aiohttp с pip env:

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

Теперь мы можем запустить наше приложение:

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

Когда мы запускаем его, мы можем указать вашему браузеру на наши различные маршруты, чтобы увидеть данные, возвращаемые нашим сервером. Если я помещу localhost:8000/planets/mars в адресную строку моего браузера, я должен увидеть следующий ответ:

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

Это то же самое, что выполнить следующую команду curl :

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

Если вы не знакомы с curl , это удобный инструмент командной строки для тестирования ваших HTTP-маршрутов.

Мы можем предоставить GET URL to curl :

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

Это дает нам эфемериды Марса в Гринвичской обсерватории в Великобритании.

Мы можем закодировать координаты в URL-адресе запроса GET , чтобы получить эфемериды Марса в других местах (обратите внимание на кавычки вокруг 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 также можно использовать для создания почтовых запросов:

[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}

Обратите внимание, что, предоставляя поле --data , curl автоматически предполагает, что мы делаем POST-запрос.

Прежде чем мы двинемся дальше, я должен отметить, что функция web.run_app запускает наше приложение блокирующим образом. Это определенно не то, чего мы хотим достичь!

Чтобы запустить его одновременно, мы должны добавить немного больше кода:

# 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())

Обратите внимание на наличие loop.run_forever вместо вызова loop.run_until_complete , который мы видели ранее. Вместо выполнения заданного количества сопрограмм мы хотим, чтобы наша программа запустила сервер , который будет обрабатывать запросы до тех пор, пока мы не выйдем с помощью ctrl+c , после чего она изящно завершит работу сервера.

Клиент HTML/JavaScript

aiohttp позволяет нам обслуживать файлы HTML и JavaScript. Использование aiohttp для обслуживания “статических” ресурсов, таких как CSS и JavaScript, не рекомендуется, но для целей этого приложения это не должно быть проблемой.

Давайте добавим несколько строк к нашему aiohttp_app.py файл для обслуживания HTML-файла, ссылающегося на файл 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("/", "./")
...

Сопрограмма hello настраивает маршрут GET на localhost:8000/ , который обслуживает содержимое index.html , расположенный в том же каталоге, из которого мы запускаем наш сервер.

Линия app.router.add_static настраивает маршрут по адресу localhost:8000/ для обслуживания файлов в том же каталоге, из которого мы запускаем наш сервер. Это означает, что ваш браузер сможет найти файл JavaScript, на который мы ссылаемся в index.html .

Примечание : В производстве имеет смысл переместить HTML, CSS и JS-файлы в отдельный каталог, который обслуживается сам по себе. Это делает его таким образом, что любопытный пользователь не может получить доступ к нашему серверному коду.

HTML файл довольно прост:






    
    
    Planet Tracker


    



Тем не менее, файл JavaScript немного более вовлечен:

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 {
            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()
})

Это приложение будет периодически (каждые 2 секунды) обновлять и отображать эфемериды планет. Мы можем предоставить наши собственные гео координаты или позволить веб-API геолокации определить наше текущее местоположение. Приложение обновляет геолокацию, если пользователь перестает печатать на полсекунды или больше.

Хотя это не учебник по JavaScript, я думаю, что полезно понять, что делают различные части скрипта:

  • create Planet Display – это динамическое создание HTML-элементов и привязка их к объектной модели документа (DOM)
  • update Planet Display принимает данные, полученные с сервера, и заполняет элементы, созданные с помощью create Planet Display
  • get делает запрос GET на сервер. Объект XMLHttpRequest позволяет сделать это без перезагрузки страницы.
  • post делает POST-запрос к серверу. Как и в случае с get это делается без перезагрузки страницы.
  • get GeoLocation использует Web Geolocation API для получения текущих географических координат пользователя. Это должно быть выполнено “в безопасном контексте” (то есть мы должны использовать HTTPS | не | HTTP ). get Planet Ephemeris и
  • get Planet Ephemerides делают GET-запросы на сервер, чтобы получить эфемериды для конкретной планеты и получить эфемериды для всех планет соответственно. тест производительности делает
  • n запросов к серверу и определяет, сколько времени это займет.

Праймер по развертыванию в Heroku

Heroku – это сервис для легкого развертывания веб-приложений. Heroku заботится о настройке веб-компонентов приложения, таких как настройка обратных прокси-серверов или забота о балансировке нагрузки. Для приложений, обрабатывающих мало запросов и небольшое количество пользователей, Heroku-отличный бесплатный хостинг.

Развертывание приложений Python в Heroku в последние годы стало очень простым. По сути, мы должны создать два файла, которые перечисляют зависимости нашего приложения и сообщают Heroku, как запустить наше приложение.

A Pip file заботится о первом, в то время как a Procfile заботится о втором. Файл трубы поддерживается с помощью pipenv – мы добавляем в ваш файл Pip (и файл трубы.lock) каждый раз, когда мы устанавливаем зависимость.

Чтобы запустить наше приложение на Heroku, мы должны добавить еще одну зависимость:

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

Мы можем создать ваш собственный профиль, добавив в него следующую строку:

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

В основном это говорит Heroku использовать Gunicorn для запуска нашего приложения, используя специальный aiohttp web worker.

Прежде чем вы сможете развернуться в Heroku, вам нужно будет начать отслеживать приложение с помощью Git:

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

Теперь вы можете следовать инструкциям на Heroku devcenter here для развертывания вашего приложения. Обратите внимание, что вы можете пропустить шаг “Подготовка приложения” этого руководства, так как у вас уже есть отслеживаемое приложение git.

Как только ваше приложение будет развернуто, вы можете перейти к выбранному URL-адресу Heroku в вашем браузере и просмотреть приложение, которое будет выглядеть примерно так:

Вывод

В этой статье мы погрузились в то, как выглядит асинхронная веб – разработка на Python- ее преимущества и использование. Затем мы построили простое реактивное приложение на основе aiohttp , которое динамически отображает текущие соответствующие координаты неба планет Солнечной системы с учетом географических координат пользователя.

После создания приложения мы подготовили его к развертыванию на Heroku.

Как уже упоминалось ранее, вы можете найти как исходный код , так и демо-версию приложения , если это необходимо.