Асинхронное программирование для веб-разработки в Python

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

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

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

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

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

Введение

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

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

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

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

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

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

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

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

asyncio

asyncio, часть стандартной библиотеки в Python, предоставляет цикл обработки событий и набор инструментов для управления им. С помощью 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, который завершается только тогда, когда завершены все его составляющие задачи или сопрограммы. Последние две строки представляют собой шаблон asyncio для запуска данной сопрограммы до ее завершения.

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

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

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

Установка aiohttp

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

pip install --user aiohttp

В следующих примерах показано, как с помощью aiohttp загрузить HTML-содержимое веб-сайта example.com:

# 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 с resp.text(), чтобы получить HTML-содержимое страницы. Если бы мы его не использовали, результат нашей программы был бы примерно таким:
[email protected]:~$ python example2_basic_aiohttp_request.py
<coroutine object ClientResponse.text at 0x7fe64e574ba0>
  • 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 выполнять их одновременно с asycio.gather.

Веб-приложение PlanetTracker

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

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

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

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

PyEphem

PyEphem – это библиотека Python, которая позволяет точно вычислять эфемериды.

Он особенно хорошо подходит для поставленной задачи, так как в нем есть обычные астрономические объекты, приготовленные в библиотеке. Сначала установим PyEphem:

[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

Учитывая некоторую широту и долготу, мы можем легко получить текущие эфемериды планеты в градусах. Теперь давайте настроим маршрут 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_ephmeris была обработчиком для маршрута GET переменной.

Прежде чем запустить это, давайте установим aiohttp с помощью pipenv:

[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-маршрутов.

Мы можем предоставить URL-адрес GET для 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.051lat=-39.754elevation=0"
{"az": 102.30273048280189, "alt": 11.690380174890928, "name": "mars"

curl также можно использовать для выполнения запросов POST:

[email protected]:~$ curl --header "Content-Type: application/x-www-form-urlencoded" --data "lat=48.93lon=2.45elevation=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("/", "./")
...

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

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

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

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

<!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>

Хотя файл 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<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()
})

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

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

  • createPlanetDisplay динамически создает элементы HTML и привязывает их к объектной модели документа (DOM).
  • updatePlanetDisplay принимает данные, полученные от сервера, и заполняет элементы, созданные createPlanetDisplay
  • get делает запрос GET к серверу. Объект XMLHttpRequest позволяет сделать это без перезагрузки страницы.
  • post отправляет POST-запрос на сервер. Как и в случае с get, это делается без перезагрузки страницы.
  • getGeoLocation использует API веб-геолокации для получения текущих географических координат пользователя. Это должно быть выполнено «в безопасном контексте» (т.е. мы должны использовать HTTPS, а не HTTP).
  • getPlanetEphemeris и getPlanetEphemerides отправляют GET-запросы к серверу, чтобы получить эфемериды для конкретной планеты и эфемериды для всех планет, соответственно.
  • testPerformance делает n запросов к серверу и определяет, сколько времени это займет.

Heroku

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

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

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

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

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

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

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

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

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

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

Обратите внимание, что вы можете пропустить этап «Подготовка приложения» в этом руководстве, так как у вас уже есть приложение, отслеживаемое git.

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

URL-адресу Heroku

Заключение

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

После создания приложения мы подготовили его к развертыванию на Heroku. Как упоминалось ранее, при необходимости вы можете найти как исходный код, так и демонстрацию приложения.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *