Асинхронное программирование хорошо подходит для задач, которые включают частое чтение и запись файлов или отправку данных с сервера и обратно. Асинхронные программы выполняют операции ввода-вывода неблокирующим образом, что означает, что они могут выполнять другие задачи, ожидая возврата данных от клиента, а не просто ждать, тратя ресурсы и время.
Python, как и многие другие языки, по умолчанию не является асинхронным. К счастью, быстрые изменения в мире ИТ позволяют нам писать асинхронный код даже с использованием языков, которые изначально не предназначались для этого. С годами требования к скорости превышают возможности оборудования, и компании по всему миру объединились с Reactive Manifesto для решения этой проблемы.
Неблокирующее поведение асинхронных программ может привести к значительному увеличению производительности в контексте веб-приложения, помогая решить проблему разработки реактивных приложений.
В Python 3 собраны несколько мощных инструментов для написания асинхронных приложений. В этой статье мы рассмотрим некоторые из этих инструментов, особенно в том, что касается веб-разработки.
Мы будем разрабатывать простое реактивное приложение на основе aiohttp для отображения текущих соответствующих небесных координат планет Солнечной системы с учетом географических координат пользователя. Вы можете найти приложение здесь , а исходный код - здесь .
В конце мы обсудим, как подготовить приложение к развертыванию на Heroku .
Введение в асинхронный Python
Для тех, кто знаком с написанием традиционного кода Python, переход к асинхронному коду может быть концептуально немного сложным. Асинхронный код в Python полагается на сопрограммы , которые в сочетании с циклом событий позволяют писать код, который может казаться выполняющим более одного действия одновременно.
Сопрограммы можно рассматривать как функции, у которых есть точки в коде, где они возвращают управление программой вызывающему контексту. Эти точки «доходности» позволяют приостанавливать и возобновлять выполнение сопрограмм в дополнение к обмену данными между контекстами.
Цикл событий решает, какой фрагмент кода запускается в любой момент - он отвечает за приостановку, возобновление и обмен данными между сопрограммами. Это означает, что части разных сопрограмм могут в конечном итоге выполняться в порядке, отличном от того, в котором они были запланированы. Идея выполнения различных фрагментов кода в произвольном порядке называется параллелизмом .
Размышление о параллелизме в контексте выполнения HTTP
запросов может
прояснить ситуацию. Представьте, что вы хотите сделать много независимых
запросов к серверу. Например, мы можем запросить веб-сайт, чтобы
получить статистику обо всех спортивных игроках в данном сезоне.
Мы могли бы сделать каждый запрос последовательно. Однако с каждым запросом мы можем представить, что наш код может некоторое время ждать, пока запрос будет доставлен на сервер, а ответ будет отправлен обратно.
Иногда эти операции могут занимать даже несколько секунд. Приложение может испытывать сетевую задержку из-за большого количества пользователей или просто из-за ограничений скорости данного сервера.
Что, если бы наш код мог делать другие вещи, ожидая ответа от сервера? Более того, что, если он вернется к обработке данного запроса только после получения данных ответа? Мы могли бы сделать много запросов в быстрой последовательности, если бы нам не приходилось ждать завершения каждого отдельного запроса, прежде чем переходить к следующему в списке.
Сопрограммы с циклом событий позволяют нам писать код, который ведет себя именно таким образом.
asyncio
asyncio , часть
стандартной библиотеки Python, предоставляет цикл обработки событий и
набор инструментов для управления им. С помощью asyncio мы можем
планировать выполнение сопрограмм и создавать новые сопрограммы (на
самом asyncio.Task
объекты asyncio.Task, используя язык 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.Task
который
завершается только тогда, когда завершены все его составляющие задачи /
сопрограммы. Последние две строки представляют asyncio
шаблон asyncio
для запуска данной сопрограммы до ее завершения.
Сопрограммы, в отличие от функций, не начинают выполняться сразу после
вызова. await
- это то, что сообщает циклу событий запланировать
выполнение сопрограммы.
Если вытаскивает 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
compute
его координат:
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
класс
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_ephmeris
сопрограмма 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.051&lat=-39.754&elevation=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.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
в 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"
Теперь вы можете следовать инструкциям в центре разработки Heroku здесь, чтобы развернуть свое приложение. Обратите внимание, что вы можете пропустить этап «Подготовка приложения» в этом руководстве, так как у вас уже есть приложение, отслеживаемое git.
После развертывания приложения вы можете перейти к выбранному URL-адресу Heroku в своем браузере и просмотреть приложение, которое будет выглядеть примерно так:
{.ezlazyload .img-responsive}
Заключение
В этой статье мы погрузились в то, как выглядит асинхронная веб-разработка на Python - ее преимущества и использование. Впоследствии мы создали простое реактивное приложение на основе aiohttp , которое динамически отображает текущие соответствующие координаты неба планет Солнечной системы с учетом географических координат пользователя.
После создания приложения мы подготовили его к развертыванию на Heroku.
Как упоминалось ранее, при необходимости вы можете найти как исходный код, так и демонстрацию приложения .