Микросервис, использующий httpio. Задействует несколько потоков. Используется python 3.7
- В одном потоке асинхронно сервер получает данные о курсе доллара, рубля и евро
- В другом потоке сервер асинхронно отвечает на HTTP запросы на порту 8080. Реализовано REST API (см. документацию)
REST API, отвечает на запросы следующего вида (тип запроса, url, payload //комментарий):
- GET /usd/get
- GET /rub/get
- GET /eur/get
- POST /amount/set {"usd":10}
- POST /amount/set {"rub":100.5, "eur":10, "usd":20}
- POST /modify {"usd":5} // добавить к текущему количеству usd 5
- POST /modify {"eur":10, "rub":-20} // добавить к текущему количеству eur 10, уменьшить текущее количество rub на 20
- GET /reverse_console_output_status // изменяет значения флага debug для вывода данных в консоль/лог
- GET /amount/get отвечает общей суммой средств для каждой из трёх валют с учётом текущего курса,
количеством каждой из валют отдельно и текущим курсом.
- пример ответа: {"rub-usd": 68.6319, "rub-eur": 77.9658, "usd-eur": 1.14, "rub": 0, "usd": 0, "eur": 0, "sum": "0.0 rub / 0.0 usd / 0.0 eur"}
- На остальные запросы в случае успеха возвращается {'result': True}
Микросервис реализован на трендовом стеке - asyncio, httpio.
Приложение состоит из модуля с абстрактным классом и второго модуля, импортирующего этот класс.
Абстрактный класс AbstractCurrencyHandler.
Класс для работы приложения CurrencyHandler
Логирование - это отдельная сущность, поэтому она вынесена в отдельный модуль logger.py
Интеграционные тесты находятся в модуле tests.py. Запускаются следующим образом: pytest -v tests.py
Логирование осуществляется через стандартный модуль logging. Аттрибуты определены в классе AbstractCurrencyHandler:
Формат лога: '%(asctime)s — %(name)s — %(levelname)s — %(message)s')
Вид лога:
2020-06-05 17:46:41,055 — currency_handler.log — DEBUG — Initial amount: {'USD': 0, 'EUR': 0, 'RUB': 0}
2020-06-05 17:46:41,055 — currency_handler.log — INFO — Server Started
2020-06-05 17:47:46,171 — currency_handler.log — INFO — USD has changed to 68.6319 EUR has changed to 77.9658
- _log_events() используется для занесения информации в лог-файл currency_handler.log
- _console_output() выводит данные в консоль
- _hook_currency_values_changed() выводит информацию в консоль и в лог файл, если изменилась стомость валюты
Запуск приложения возможен с аргументами командной строки.
Можно задать 5 аргументов:
- --eur - количество евро (по-умолчанию 0)
- --rub - количество рублей (по-умолчанию 0)
- --usd - количество долларов (по-умолчанию 0)
- --period - частота в секундах опроса удаленного сервера с данными о курсах валют (по-умолчанию 5 )
- --debug - режим дебага (по-умолчанию False). Возможные значения для перевода в True:
- 1, True, true, y, Y
В случае, если debug не передается, вся информация о запросах сервера записывается в лог файл.
Если задается не falsy значение параметра debug, то данные о запросах удаленного информационного сервера печатаются в консоль, а также данные POST-запросов к серверу выводятся в консоль.
Можно изменить поведение приложение вызвав API /reverse_console_output_status
Для анализа и парсинга аргументов командной строки используется модуль argparse
Примеры запуска приложения из командной строки:
python currency_handler.py --eur 10 --usd 5 --rub 100 --period 5 --debug true
python currency_handler.py --eur 10 --debug true
python currency_handler.py --eur 10
python currency_handler.py
-
В данный момент для логгирования и вывода данных при дебаге используется три сущности. Можно изменить это поведение добавив Handler для консоли, т.е. реализовав внутри класса Logger функцию get_file_handler() и использовать этот обработчик:
self.logger.addHandler(Logger._get_file_handler()) -
Традиционная проблема при работе с asyncio и потоками - задача graceful shutdown, в данный момент этого нет. При попытке с помощью ctrl+c прервать работу приложения не останавливается event-loop и запущенный поток. Проблема усугубляется тем, что в потоке запущен отдельный event-loop. Event-loop, который запрашивает курсы валют можно остановить так:
try:
loop.run_until_complete()
except KeyboardInterrupt as e:
print("Caught keyboard interrupt. Graceful shutdown...")
finally:
loop.close()
при этом вероятна ошибка asyncio cannot close a running event loop, потому что после запуска начнутся сетевые I/O операции.
Непонятно, как при этом быть с запущенным потоком. Можно было бы создать собственный класс, унаследованный от Thread традиционным образом и переопределить метод run(), но внутри запущен loop..
- Внутри класса CurrencyHandler корутина fetch_currency запрашивает данные у информационного сервера с курсами валют. Нужно добавить несколько попыток запроса, ограниченные значением max_retries, в том случае, если запросы падают и удаленный сервер не отдает данные.
Приложение написано с огромным уважением к PEP8, но кое-где не соблюдается максимальная длина строк. Впрочем, внутри стандартных модулей Python есть стилистические нарушения значительно серьезнее..
Как написано в самом первоисточнике "sometimes style guide recommendations just aren't applicable. When in doubt, use your best judgment".