Показаны сообщения с ярлыком tips and tricks. Показать все сообщения
Показаны сообщения с ярлыком tips and tricks. Показать все сообщения

8 января 2020 г.

Базовая настройка aiohttp.web приложений

Словил себя на мысли, что несмотря на то, что большинство моих веб-приложений работают на aiohttp.web, их настройка происходит в лучших Django традициях,

  1. Определение настроек в settings.py модуле, как DEBUG = to_bool(os.getenv("DEBUG") or False)
  2. Импорт целого модуля и сохранение его в aiohttp.web приложении, как types.MappingTypeProxy структура
  3. Использование сохраненного маппинга во вью-функциях, как, например, request.config_dict["settings"]["DEBUG"]

И если этот подход работал like a magic 5 лет назад, не говорит о том, что он идеально вписывается в Python в 2020 году. Главные проблемы с тем, как это было сделано ранее, это:

  • Инстинктивно хочется сделать from app import settings и потом просто settings.DEBUG, вместо того чтоб идти за настройками в request.config_dict
  • Работать с настройками, которые хранятся как Mapping[str, Any] — это выстрел себе в обе ноги. Никакой mypy не подскажет тебе, есть ли у тебя в настройках ключ VERY_IMPORTANT_KEY и не ошибься ли ты с тем, что считаешь, что PRICE_MULTIPLICATOR это decimal.Decimal, а не float.

Что же делать? Как обычно есть два пути, взять что-то готовое или написать свое. Второй путь, куда более заманчивый, поэтому план такой,

  1. Нужно хранить настройке в какой-то структуре данных, а что может быть лучше @attr.dataclass для этого?
  2. Нужно упростить получение настроек из окружения для attr.ib (аттрибутов) структуры данных
  3. Ну и куда же без mypy, так что нужно убедиться, что структура данных с настройками будет поддерживать любые типы данных и работать без магических # type: ignore

План рабочий, поэтому на его реализацию много времени не понадобилось. Начал с env_factory:

import os
from typing import Optional, overload, TypeVar

import attr
from rororo.settings import to_bool


T = TypeVar("T")


@overload
def env_factory(name: str) -> Optional[str]:
    ...


@overload
def env_factory(name: str, default: T) -> T:
    ...


def env_factory(name: str, default: T = None) -> Union[Optional[str], T]:
    def getenv() -> Union[Optional[str], T]:
        value = os.getenv(name)
        if default is None:
            return value

        if value is None:
            return default

        expected_type = type(default)
        if isinstance(value, expected_type):
            return value

        try:
            if expected_type is bool:
                return to_bool(value)  # type: ignore
            return expected_type(value)  # type: ignore
        except (TypeError, ValueError):
            raise ValueError(
                f"Unable to convert {name} env var to {expected_type}."
            )

    return attr.Factory(getenv)

Продолжил имплементацией хранения базовых настроек,

import attr

from rororo.settings import setup_locale, setup_logging, setup_timezone


@attr.dataclass(frozen=True, slots=True)
class BaseSettings:
    # Base aiohttp settings
    host: str = env_factory("AIOHTTP_HOST", "localhost")
    port: int = env_factory("AIOHTTP_PORT", 8080)

    # Base application settings
    debug: bool = env_factory("DEBUG", False)
    level: Level = env_factory("LEVEL", "dev")

    # Date & time settings
    time_zone: str = env_factory("TIME_ZONE", "UTC")

    # Locale settings
    first_weekday: int = env_factory("FIRST_WEEKDAY", 0)
    locale: str = env_factory("LOCALE", "en_US.UTF-8")

    # Sentry settings
    sentry_dsn: Optional[str] = env_factory("SENTRY_DSN")
    sentry_release: Optional[str] = env_factory("SENTRY_RELEASE")

    def apply(
        self,
        *,
        loggers: Iterable[str] = None,
        remove_root_handlers: bool = False,
    ) -> None:
        if loggers:
            setup_logging(
                default_logging_dict(*loggers),
                remove_root_handlers=remove_root_handlers,
            )

        setup_locale(self.locale, self.first_weekday)
        setup_timezone(self.time_zone)

И в конце концов осталось только начать использовать это все в create_app фабриках и во вью-функциях.

def create_app(argv: List[str] = None, **options: Any) -> web.Application:
     settings = Settings()
     settings.apply()

     app = web.Application(...)
     app["settings"] = settings

     return app
async def index(request: web.Request) -> web.Response:
     settings: Settings = request.config_dict["settings"]  # For JEDI autocomplete
     if settings.debug:
         print("Hello, world!")
     return web.json_response(True)

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

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

pss. Если вы заметили, то env_factory содержит два # type: ignore комментария. Хотел бы объяснить зачем они там. Первый на линии с to_bool вызовом из-за того, что mypy не понимает, что T is bool и поэтому выдает ошибку Incompatible return value type. Ну а во втором случае mypy ругается на Too many arguments for "object". Отакої

10 марта 2019 г.

Сортировка списка с unicode strings

Думал, что после стольких лет Python уже не удивит меня, однако пословица Век живи, век учись стала для меня как никогда актуальной вчера. Задача была весьма простая: отсортировать список с строками, где строка - это украинское имя, то есть вроде бы все должно быть предельно просто используя Python 3.7.2:

data = ['Андрій', 'Ігор', 'Євген', 'Віталій']
assert sorted(data) == ['Андрій', 'Віталій', 'Євген', 'Ігор']

Но нет, not so fast!

$ python
Python 3.7.2 (default, Jan  4 2019, 12:23:06) 
[Clang 10.0.0 (clang-1000.10.44.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> data = ['Андрій', 'Ігор', 'Євген', 'Віталій']
>>> sorted(data)
['Євген', 'Ігор', 'Андрій', 'Віталій']

Интересно. Значит что-то не так с локалью. Нужно установить правильную локаль и попробовать еще раз.

$ python
Python 3.7.2 (default, Jan  4 2019, 12:23:06) 
[Clang 10.0.0 (clang-1000.10.44.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import locale
>>> locale.setlocale(locale.LC_ALL, 'uk_UA.UTF-8')
'uk_UA.UTF-8'
>>> locale.setlocale(locale.LC_COLLATE, 'uk_UA.UTF-8')
'uk_UA.UTF-8'
>>> data = ['Андрій', 'Ігор', 'Євген', 'Віталій']
>>> sorted(data)
['Євген', 'Ігор', 'Андрій', 'Віталій']

Хм. Странно. Значит нужно пойти почитать доку locale и найти там функцию, которая будет делать locale compare. Точно же есть такая.

$ python
Python 3.7.2 (default, Jan  4 2019, 12:23:06) 
[Clang 10.0.0 (clang-1000.10.44.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import locale
>>> locale.setlocale(locale.LC_ALL, 'uk_UA.UTF-8')
'uk_UA.UTF-8'
>>> locale.setlocale(locale.LC_COLLATE, 'uk_UA.UTF-8')
'uk_UA.UTF-8'
>>> data = ['Андрій', 'Ігор', 'Євген', 'Віталій']
>>> sorted(data, key=locale.strxfrm)
['Ігор', 'Євген', 'Андрій', 'Віталій']
>>> import functools
>>> sorted(data, key=functools.cmp_to_key(locale.strcoll))
['Ігор', 'Євген', 'Андрій', 'Віталій']

Та ладно. Не верю. Все должно работать. Аргх.

После того, как эмоции уляглись, вспоминаю про ICU и думаю, ну ок, точно есть биндинги ICU для Python и там должна быть функция, которую можно будет скормить в key для правильной сортировки значений в списке.

  1. Устанавливаем ICU в систему,
    • Для macOS:
      $ brew install icu4c
      $ export PATH="/usr/local/opt/icu4c/bin:$PATH"
    • Для Ubuntu Linux:
      # apt install libicu-dev icu-devtools
  2. Устанавливаем PyICU,
    $ poetry add PyICU
    или по старинке:
    $ pip install PyICU
$ python
Python 3.7.2 (default, Jan  4 2019, 12:23:06) 
[Clang 10.0.0 (clang-1000.10.44.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import icu
>>> collator = icu.Collator.createInstance(icu.Locale('uk_UA.UTF-8'))
>>> data = ['Андрій', 'Ігор', 'Євген', 'Віталій']
>>> sorted(data, key=collator.getSortKey)
['Андрій', 'Віталій', 'Євген', 'Ігор']

Фух! Работает! Так что да, чистая правда: век живи, век учись!

ps. В комментариях @xnull поделился еще одним способом сортировки при помощи pyuca библиотеки, которая не требует установки в систему никаких дополнительных зависимостей.

$ poetry add pyuca  # pip install pyuca
$ python
Python 3.7.2 (default, Jan  2 2019, 13:30:18) 
[Clang 10.0.0 (clang-1000.10.44.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import pyuca
>>> coll = pyuca.Collator()
>>> data = ['Євген', 'Ігор', 'Андрій', 'Віталій']
>>> sorted(data, key=coll.sort_key)
['Андрій', 'Віталій', 'Євген', 'Ігор']

16 июля 2018 г.

Настройка Sublime Text 3 для Python разработки

Выбор редактора (среды для разработки), наверное, главное событие в жизни каждого разработчика. Я тоже поначалу не мог долго определиться какой редактор использовать, но потом, лет 8-9 назад установил Sublime Text и все - вопрос с редактором был закрыт раз и навсегда. Да, за эти года я иногда пробовал научиться работать в vim, но не смог, ну а на новомодные Atom, PyCharm или VS Code я вообще не смотрел. Поэтому я хотел бы рассказать про то, как я использую Sublime Text 3 и почему мне не нужен IDE в 2018 году.

Главная причина, почему мне не нужен IDE - это, конечно, база расширений для Саблайма, которая находится на packagecontrol.io и покрывает любой из языков, на которых я программирую. Так как блог о Python, то я расскажу только о расширениях для Python, но у меня также есть установленные плагины для JavaScript, flow, TypeScript, Elm и Rust.

В Python же все начинается с установки MagicPython от Юрия Селиванова, расширения, которое добавляет поддержку подсветки всех всех новомодных фич Python, и хоть авторы Sublime Text пытаются улучшать поддержку базового Python от релиза к релизу, нам это не нужно после установки и настройки MagicPython.

Дальше. Было бы неплохо иметь автодополнение для Python, да? Поэтому следующим делом, устанавливаем SublimeJEDI от Сережи Русских. Благодаря движку JEDI, который используется в IPython, к примеру, на выходе мы получаем автодополнение нашей мечты. Но для этого в настройках проекта нам нужно сделать две вещи,

  1. Задать путь к python_interpreter, в общем случае это "~/Projects/<project>/.venv/bin/python", чтоб JEDI использовал все зависимости, установленные в проекте
  2. Настроить python_package_paths. Опять же в общем случае это ["~/Projects/<project>"]

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

Идем дальше. Линтинг. Тут нам нужны SublimeLinter-flake8 и SublimeLinter-contrib-mypy (а куда без type annotations в 2018 то году?). По дефолту, они будут пытаться использовать стандартный Python, установленный в системе, но так как в моем случае набор flake8 плагинов от проекта к проекту отличается, я задаю executable для этих линтеров в файле настройки проекта (<project>.sublime-project) как,

  1. "SublimeLinter.linters.flake8.executable": "~/Projects/<project>/.venv/bin/flake8" для настройки SublimeLinter-flake8
  2. "SublimeLinter.linters.mypy.executable": "~/Projects/<project>/.venv/bin/mypy" для настройки SublimeLinter-contrib-mypy

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

По большому счету с главными расширениями, которые превращают Sublime Text 3 в удобную среду разработки на Python мы закончили, но мне также хотелось бы упомянуть еще некоторые плагины, которые могут точно пригодиться,

  • sublack - автоформатирование Python файлов при помощи black. Нужная вещь, когда вы работаете в команде и не хотите на код ревью тратить время о спорах насчет форматирования Python кода.
  • editorconfig-sublimetext - также в 2018 году не зачем спорить о табах vs пробелах или отступах. Единократно кладем .editorconfig в корень проекта, а дальше Sublime Text 3 автоматически применяет необходимые настройки форматирования для любых файлов.
  • DocBlockr - упрощает работу с комментариями в коде. Конечно, этот плагин больше применим для JavaScript кода, но и для Python иногда бывает полезен.
  • GitGutter - просмотр того, как изменился блок кода, прямо в Sublime Text. Очень нужная вещь, для разработчиков, которые не любят бессмысленный закомментированный старый код.
  • Terminus - и хоть я постоянно держу открытым Terminal.app иногда бывает удобно открыть терминал прямо в соседней вкладке Sublime Text.

Ну и напоследок чуть о внешнем виде. Все это время я использую схему подсветки кода Tomorrow-Night, так что очень был рад, когда настроил Boxy Theme для внешнего вида Sublime Text, используя это же цветовое решение. В итоге, мой Sublime Text выглядит следующим образом,

А вот шаблон настроек, который я использую для своих проектов,

{
    "folders":
    [
        {
            "file_exclude_patterns": [
                ".install*",
                "*.lock",
                "*-lock.json"
            ],
            "folder_exclude_patterns": [
                ".*cache",
                "node_modules"
            ],
            "path": "/Users/playpauseandstop/Projects/<project>"
        }
    ],
    "settings": {
        "python_interpreter": "/Users/playpauseandstop/Projects/<project>/.venv/bin/python",
        "python_package_paths": [
            "/Users/playpauseandstop/Projects/<project>"
        ],
        "SublimeLinter.linters.flake8.executable": "/Users/playpauseandstop/Projects/<project>/.venv/bin/flake8",
        "SublimeLinter.linters.mypy.executable": "/Users/playpauseandstop/Projects/<project>/.venv/bin/mypy"
    }
}

Вот и получается, что Sublime Text 3 с таким набором расширений - это прекрасный редактор для программирования на Python.

28 июня 2017 г.

Fast Python. Парсинг ISO дат

Преобразование ISO-даты из строки в объект datetime.datetime (или datetime.date), наверное, одна из самых распространенных и постоянных задач в web-разработке на Python. Количество способов сделать это просто поражает воображение,

In [1]: value = '2017-06-28T16:59:27+0000'

In [2]: import datetime

In [3]: datetime.datetime.strptime(value, '%Y-%m-%dT%H:%M:%S%z')
Out[3]: datetime.datetime(2017, 6, 28, 16, 59, 27, tzinfo=datetime.timezone.utc)

In [4]: from dateutil.parser import parse

In [5]: parse(value)
Out[5]: datetime.datetime(2017, 6, 28, 16, 59, 27, tzinfo=tzutc())

In [6]: import dateparser

In [7]: dateparser.parse(value)
Out[7]: datetime.datetime(2017, 6, 28, 16, 59, 27, tzinfo=)

Использовались функции: datetime.datetime.strptime, dateutil.parser.parse и dateparser.parse.

А ведь еще есть,

In [8]: import arrow

In [9]: arrow.get(value, 'YYYY-MM-DDTHH:mm:ssZZ')
Out[9]: <Arrow [2017-06-28T16:59:27+00:00]>

In [10]: import maya

In [11]: maya.parse(value)
Out[11]: <MayaDT epoch=1498669167.0>

-//-: arrow.get и maya.parse.

И это только вершина айсберга, потому что библиотек для работ с датами в Python не просто много, а очень много (что говорить, если у Django вообще есть своя parse_datetime функция, основанная на регекспе). У той или иной библиотеки работы с датами есть свои плюсы и особенности, но что делать, если нужно быстро конвертировать строки с датами в стандартные объекты datetime.datetime? Как всегда ответ прост: использовать что-то написанное на C с биндингами в Python, в нашем случае - это ciso8601,

In [12]: ciso8601.parse_datetime(value)
Out[12]: datetime.datetime(2017, 6, 28, 16, 59, 27, tzinfo=)

Результаты %timeit - красноречивы,

In [13]: %timeit datetime.datetime.strptime(value, '%Y-%m-%dT%H:%M:%S%z')
17.9 µs ± 516 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

In [14]: %timeit parse(value)
99 µs ± 1.1 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

In [15]: %timeit dateparser.parse(value)
1.61 ms ± 39.2 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

In [16]: %timeit ciso8601.parse_datetime(value)
2.08 µs ± 67.6 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

Итого: Если нужно быстро парсить ISO даты: используйте ciso8601.parse_datetime, если нужно быстро парсить не ISO даты: сначала попробуйте стандартный datetime.datetime.strptime, потом dateutil.parser.parse и только в случае особой и острой необходимости dateparser.parse и прочие библиотеки, основанные на нем.

ps. Результаты приведены для Python 3.6.1, на Python 2.7.13 соблюдаются такие же показатели, но в 2.7.13 в стандартном strptime не было возможности разобрать +0000, потому что ValueError: 'z' is a bad directive in format '%Y-%m-%dT%H:%M:%S%z'.

11 марта 2015 г.

Fast Python. Выпуск 1. Обновление словарей

Привет! Запускаю раздел Fast Python, в котором буду делиться простыми рецептами про то, как ускорить и оптимизировать выполнение кода на Python.

Первый выпуск будет посвящен обновлению данных в словарях. Словари - одни из найболее часто используемых типов данных в Python. И сколько времени я с ними не работаю, у меня никогда не возникал вопрос как правильней обновить данные в словаре. Я для себя всегда отвечал на него, что правильней обновлять используя метод update, но сегодня с утра наткнулся на твит от Брэда Монтгомери и мой мир изменился.

Оказывается, что более правильным с точки зрения скорости является обновление словаря через поочередное присваивание значений, чем единичный апдейт. Не верите? Результаты замеров весьма красноречивы:

Python 3.4.3

In [2]: timeit("d = {}; d.update({'first': 'first', 'second': ['one', 'two', 'three'], 'third': True})", number=1000000)
Out[2]: 0.9664371280086925

In [3]: timeit("d = {}; d['first'] = 'first'; d['second'] = ['one', 'two', 'three']; d['third'] = True", number=1000000)                                      
Out[3]: 0.4200690069992561

Python 2.7.9

In [2]: timeit("d = {}; d.update({'first': 'first', 'second': ['one', 'two', 'three'], 'third': True})", number=1000000)
Out[2]: 1.0035829544067383

In [3]: timeit("d = {}; d['first'] = 'first'; d['second'] = ['one', 'two', 'three']; d['third'] = True", number=1000000)
Out[3]: 0.5186400413513184

Bonus: PyPy 2.4.0

>>>> timeit("d = {}; d.update({'first': 'first', 'second': ['one', 'two', 'three'], 'third': True})", number=1000000)                                               
0.10811400413513184
>>>> timeit("d = {}; d['first'] = 'first'; d['second'] = ['one', 'two', 'three']; d['third'] = True", number=1000000)                                         
0.005925893783569336

Итого: Если вы где-то в коде увидите обновление одиночного или множественных элементов словаря через .update - смело переписывайте этот фрагмент на использование __setitem__.

Fast Python:

data['key'] = 'value'
data['another-key'] = 'another-value'

Slow Python:

data.update({
    'key': 'value',
    'another-key': 'another-value',
})

UPD: Михаил Кривушин (deepwalker) в комментариях объяснил почему так происходит:

По дороге создается еще один словарь, и потом уже из него копируются элементы
https://gist.github.com/Deepwalker/c52a74c26df0707dd303

UPD2: Если ключи словаря не используют специальных символов и являются валидными Python ключами, то лучше использовать синтаксис .update(key='value'), вместо .update({'key': 'value'}):

Python 3.4.3

In [2]: timeit("d = {}; d.update(first='first', second=['one', 'two', 'three'], third=True)", number=1000000)
Out[2]: 0.88018084000214

Python 2.7.9

In [2]: timeit("d = {}; d.update(first='first', second=['one', 'two', 'three'], third=True)", number=1000000)
Out[2]: 0.7820448875427246

PyPy 2.4.0

>>>> timeit("d = {}; d.update(first='first', second=['one', 'two', 'three'], third=True)", number=1000000)
0.010169029235839844

16 февраля 2015 г.

Upgrade your pip & virtualenv now

Странно, что несмотря на очень давний выход pip 6.0 и virtualenv 12.0, многие Python разработчики все еще сидят на более ранних версиях этих незаменимых утилит.

Мой вам совет - обновляйте свой pip & virtualenv сейчас же!

Главная причина - это, конечно, встроенный в pip, толковый и включенный по умолчанию менеджер скачанных зависимостей. Да, и раньше можно было пользоваться опцией --download-cache или конфигом:

[install]
download_cache = /path/to/pip-cache

в ~/.pip/pip.conf, но старый менеджер загрузок был скорее дополнительным, чем полностью готовым к использованию механизмом. Более детальный ход разработки менеджера загрузок хорошо продемонстрирован на GitHub.

Из остального в новом pip ведется проверка версий относительно PEP 440, а значит, что, к сожалению, лучше попрощаться с версиями: X.Y-dev, которые хоть и не попадают на PyPI, но очень часто используются в разработке.

25 августа 2014 г.

Celery воркер зависает на "mingle: searching for neighbors"

Сегодня ВНЕЗАПНО все Celery воркеры перестали принимать любые задачи, при том что RabbitMQ (используется в качестве брокера) очереди были пустыми, а остальные части системы функционировали нормально. Не помогала ни перезагрузка Celery воркеров, ни перезагрузка RabbitMQ сервера.

После недолгого копания по логам проблема была локализирована, любой Celery воркер как будто зависал на моменте поиска соседей, оставляя в логах что-то такое:

2014-08-25 14:29:12 [INFO:celery.worker.consumer] Connected to amqp://guest:**@127.0.0.1:5672//
2014-08-25 14:29:12 [INFO:celery.worker.consumer] mingle: searching for neighbors

Быстрый гуглинг указал на существующую Celery issue, а уже в ней и нашелся ответ на проблему. Оказывается на корневом разделе подошло к концу место (было доступно порядка 150 Мб) и Celery в связке с RabbitMQ зависала не показывая никаких признаков жизни. Все починилось банальной чисткой корневого раздела, но осадок остался и на себя, что не поставил уведомления о заканчивающемся месте на корневом разделе и на Celery, что она никаким образом не пытается обработать эту ситуацию и проблему приходится вычислять окольными путями.

4 мая 2014 г.

Range для не целых чисел

Почему-то никогда не думал, что range (и xrange) понимают только целые числа (в Python 3 ситуация такая же) и функцией нельзя воспользоваться, как например:

range(0., 5., .5)
range(Decimal('0'), Decimal('10'), Decimal('1.5'))

Поэтому пришлось сделать свою замену:

import operator


def arange(start, stop=None, step=None):
    """
    Implement range function not only for integers as Python's builtin
    function, but for Decimals and floats as well.

    Returns generator with arithmetic progession, not list.
    """
    klass = type(start)
    lt_func = operator.lt

    stop = start if stop is None else stop
    start = klass(0) if start == stop else start
    step = klass(1 if step is None else step)

    assert isinstance(stop, klass), (
        'Start and stop limits have different types, {0!r} != {1!r}.'.
        format(type(start).__name__, type(stop).__name__)
    )
    assert step, "Step shouldn't be a zero: {0!r}.".format(step)

    if start < stop and step < 0 or start > stop and step > 0:
        raise StopIteration
    elif start > stop and step < 0:
        lt_func = operator.gt

    while lt_func(start, stop):
        yield start
        start += step

В отличии от range в Python 2 arange отдает генератор, а не список, что вообщем-то удобней и правильней, ну и функция всегда вернет элементы такого же типа, как были переданы в start и stop аргументы, даже если шаг - это целое число или он не указан вовсе.

Как всегда код оформлен как Gist на GitHub'е, там еще доктесты и юниттесты.

20 декабря 2012 г.

Запускаем gunicorn из Python'а

Иногда бывает надо запустить gunicorn внутри Python скрипта, например, в manage.py. Конечно всегда можно воспользоваться subprocess.call:

import subprocess


app = 'package.module:app'
host, port = '0.0.0.0', 8000
subprocess.call('gunicorn -b {}:{:d} -w 4 {}'.format(host, port, app))

Но как-то это не комильфо подумал я и решил найти более труЪ-способ :)

Решение пришло не сразу, но пришло, надо всего лишь переопределить sys.argv и вызвать метод run,

import sys

from gunicorn.app.wsgiapp import run


app = 'package.module:app'
host, port = '0.0.0.0', 8000
sys.argv = [
    sys.argv[0],
    '-b', '{}:{:d}'.format(host, port),
    '-w', '4',
    app
]
run()

Не очень круто вышло, не находите? А все потому что gunicorn еще использует устаревший optparse для парсинга аргументов с коммандной строки, а там задавать список аргументов в parse_args совсем не обязательно, ведь по дефолту берется список sys.argv[1:].

Так что на таком простом примере лично мне ясно видно, насколько optparse устарел и как вашему приложению, которое его еще использует надо сломя голову переезжать на argparse! И это я еще не рассказал вам о бесподобном управлении под-коммандами в argparse :)

29 ноября 2012 г.

Чиним gunicorn'овский Internal Server Error

Если вдруг, после запуска gunicorn перестал работать и показывает ошибку похожую на:

Internal Server Error

Traceback:

Traceback (most recent call last):
  File "/Users/playpauseandstop/Projects/project/env/lib/python2.7/site-packages/gunicorn/workers/async.py", line 45, in handle
    self.handle_request(req, client, addr)
  File "/Users/playpauseandstop/Projects/project/env/lib/python2.7/site-packages/gunicorn/workers/async.py", line 73, in handle_request
    resp, environ = wsgi.create(req, sock, addr, self.address, self.cfg)
  File "/Users/playpauseandstop/Projects/project/env/lib/python2.7/site-packages/gunicorn/http/wsgi.py", line 161, in create
    path_info = path_info.split(script_name, 1)[1]
IndexError: list index out of range

то это говорит о том, что какая-то сволочьое-то приложение установило переменную окружения SCRIPT_NAME, которую gunicorn не может нормально обработать. Чтобы пофиксить, просто удаляем переменную окружения,

$ unset SCRIPT_NAME

и перегружаем страницу (ну можно еще перегрузить gunicorn для пущей важности). That's all!

27 октября 2012 г.

Изменяем заголовки по умолчанию в имейлах от Flask-Security

Flask-Security - незаменимое расширение, если вам надо быстро и безболезнено реализовать поддержку регистрации, логина, восстановления пароля для вашего Flask приложения.

Однако с ним есть маленькая беда. Дело в том, что все заголовки писем, отправляемых при его помощи захардкожены как строки в коде, а для того или иного приложения есть смысл в замене темы сообщения 'Welcome' на 'Подтвердите ваш аккаунт' и тд.

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

appname/app.py

from flask import Flask
from flask.ext.security import Security

from appname.utils import send_mail


app = Flask('appname')
...
security = Security(app, datastore)
app.extensions['security']._send_mail_task = send_mail

appname/utils.py

from flask import current_app
from flask.ext.babel import gettext as _


def send_mail(message):
    if message.subject == 'Welcome':
        message.subject = _('Confirm your account')

    mail = current_app.extensions['mail']
    mail.send(message)

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

UPD. Не только меня волновала эта проблема и посему благодаря фиксам Anthony Plunkett в версии 1.5.1 заголовки сообщений от Flask-Security можно задавать в настройках проекта как:

SECURITY_EMAIL_SUBJECT_REGISTER = 'Confirm your account'

Все доступные настройки описаны в документации.

18 июля 2012 г.

Перенаправляем потоки в pbs

Мне нравится pbs. Эта библиотека с лихвой заменяет мне необходимость программировать всякие мелкие штуки на чистом bash'е. Однако до сегодня я не особо понимал как реализовать перенаправление потоков. И вот я наконец-то понял, о чем с вами и поделюсь :)

Итак, предположим, что нам неодходимо эмулировать следующую конструкцию:

$ git archive HEAD --format=zip --prefix=project/ > ../project.zip

В pbs это будет выглядеть как-то так:

handler = open('../project.zip', 'wb+')
pbs.git('archive', 'HEAD', format='zip', prefix='project', _out=handler)
handler.close()

Также просто будет перенаправить stderr в необходимый нам файл, например:

$ stat does_not_exist.txt 2> error_log
handler = open('error_log', 'wb+')
try:
    pbs.stat('does_not_exist.txt', _err=handler)
except pbs.ErrorReturnCode:
    pass
handler.close()

Вот такие пироги! Пользуйтесь pbs, это по-настоящему удобная и простая библиотека!

13 июля 2012 г.

Копируем виртуальные окружения с помощью virtualenv-clone

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

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

$ virtualenv --distribute --system-site-packages env
$ . env/bin/activate
(env)$ pip install -r requirements.txt
(env)$ deactivate
$ cp project/settings_local.py{.deploy,}

И вроде бы все работает, но чем больше становится зависимостей и чем больше коммитов приходит на деплой тем система начинает работать все медленней и медленней и постоянно задыхается на этапе pip install -r requirements.txt. Не хорошо!

Потом появляется решение: надо просто сделать основное виртуальное окружение, а потом для каждого коммита копировать его и накатывать уже свежие изменения файла зависимостей и вуаля. Но вместе с решением приходит и вопрос, а как-то это реализовать? Чтоб оно работало то?

Первая идея была очень интересной: а что если новое виртуальное окружение создавать в активированом основном виртуальном окружении? YO DAWG, не иначе. В итоге получилось так же если б мы просто создали новое виртуальное окружение, локальные зависимости основного окружения не были доступны в новом окружении.

Второй идеей было использование --relocatable опции для основного окружения. Результат: после копирования окружения и установки зависимостей, зависимости обновлялись и в основном окружении, а это нам совсем не нужно.

Третьей идеей было таки спросить у гугла "python virtualenv copy" на что гугл выдал мне ссылку на virtualenvwrapper, откуда я путем быстрого рисёрча исходников попал на virtualenv-clone и радостно захлопал в ладоши - то, что нужно!

Так что задача полноценного копирования виртуальных окружений решается просто:

$ sudo pip install virtualenv-clone
$ virtualenv-clone env_base env_new

зы. Еще одним решением для ускорения установки зависимостей была, есть и остается опция --download-cache для pip'а. Но хотелось именно решить вопрос с копированием виртуальных окружений.

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

17 мая 2012 г.

Постоянные сессии во Flask'е, один из способов

По умолчанию, все содержимое flask.session будет очищено при закрытии браузера. Однако много когда нам нужно, чтоб данные сессии хранились и после рестарта браузера. Для этих случаев есть аттрибут permanent и следующий простой сниппет:

import datetime

from flask import Flask, session


app = Flask(__name__)
app.before_request(lambda: setattr(session, 'permanent', True))
app.permanent_session_lifetime = datetime.timedelta(days=14)

Последняя строчка сниппета выставляет длину сессии в 14 дней, во Flask'е же по дефолту используется 31 день для хранения постоянной сессии. Также эту настройку можно указать как PERMANENT_SESSION_LIFETIME в вашем settings.py.

зы. Однако также не забывайте, что Flask хранит все данные сессии в кукисах, а не как, например, Django только ключ сессии, а все данные уже считывает с базы данных или другого источника. Так что уместно будет использовать flask.session как хранилище каких-то ключей, например, токена текущего залогинненого пользователя.

2 мая 2012 г.

Окончательно дружим Flask и nosetests

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

Как мы все знаем nosetests по дефолту захватывает все из stdout/stderr и логгинга, чтоб при запуске тестов вывод не засорялся ненужной нам информацией. Однако в дебаг-моде Flask кладет на всех и устанавливает с помощью flask.logging.create_logger функции хэндлер, который начинает срать в консоль при каждом удобном случае, причем минуя все ранее установленные хэндлеры. Итог: куча ненужной логгинг информации при запуске тестов как:

(env)$ TESTING=1 nosetets -c -v -w <package>

Не хорошо, но In mock we trust, так что все что надо - это замокать упомянутую выше функцию в случае, когда мы запускаем тесты в дебаг-моде:

if TESTING and DEBUG:
    from flask import logging as flask_logging

    def mock_create_logger(app):
        return logging.getLogger(app.logger_name)

    flask_logging.create_logger = mock_create_logger

Помещаем этот сниппет в settings.py, затем не забываем загрузить настройки как import settings; app.config.from_object(settings) в нашем app.py - и получаем счастье, nosetests уверенно захватывает все нужное и вывод тестов чист и аккуратен.

Полный гист доступен на Гитхабе, если кто-то готов предложить более красивый вариант решения проблемы - жду в комментариях.

11 марта 2012 г.

Один маленький совет для ускорения бутстрапа проектов

Конечно, использование git:// урлов в файле зависимостей проекта не есть отличная идея, но если вы таки решились на нее и даже делаете как-то так:

-e git://github.com/user/repo.git@commit#egg=package

то я спешу вас облагоразумить! Не делайте так! НИКОГДА :)

В случае в репозиториями GitHub'а, используйте zipball'ы (tarball'ы нормально не распознаются pip),

https://github.com/user/repo/zipball/commit#egg=package

в других случаях просто архивируйте при помощи git archive необходимый вам коммит или тег, и грузите его на свой cdn. И теперь вам не надо будет ожидать пока пип склонирует репо и поставит хедом необходимый вам коммит. Ускорение бутстрапа на жирных зависимостях (например, ask/celery, django/django будет очень ощутимым)!

зы. И да, даже для мастера (любого другого бранча) вам не нужно использовать git:// в случае GitHub'а. Используем такую же технику и получаем:

https://github.com/user/repo/zipball/master#egg=package

вместо:

-e git://github.com/user/repo.git#egg=package

27 января 2012 г.

Пару заметок о coverage, nosetests и lettuce

Думаю ни для кого не секрет, что в nosetests уже есть встроенная поддержка coverage, однако там нет очень важной фичи coverage, а именно возможности дополнять файл данных после каждого следующего запуска тестов (опция -a --append в coverage run).

Зачем это может понадобится? Самый простой пример: если в проекте есть и юнит тесты, и интеграционные, и не дай бог тесты, работающие с реальными данными :) Т.е. если мы запускаем все тесты перед деплоем не просто nosetests ..., а связкой из nosetests ... && nosetests ... && nosetests ..., в таком случае добавление --with-coverage в каждую итерацию nosetests даст нам три совсем ненужных таблицы покрытия, где каждая таблица будет отличаться от предыдущей и смерджить их воедино не выйдет - а это совсем не то, что нам надо.

Что делать? На самом деле ничего сложного, просто меняем:

$ nosetests --with-coverage ... && \
  nosetests --with-coverage ... && \
  nosetests --with-coverage ...

на:

$ coverage run `which nosetests` ... && \
  coverage run -a `which nosetests` ... && \
  coverage run -a `which nosetests` ... && \
  coverage report -m

В итоге получим то, чего добивались, а именно таблицу покрытия кода всеми тестами.

И да, раз уже заговорили про coverage, вы не забываете про использование .coveragerc? Очень полезная вещь!

Так что теперь, если в проекте используются и lettuce, и nosetests - посчитать покрытие кода не составит особого труда, используя coverage run `which lettuce` ... && coverage run -a `which nosetests` ...

И последнее на сегодня, при запуске lettuce тестов, не забывайте указывать путь не к директории, в которой есть features, а к самой директории features. Я на этом моменте очень сильно злился!

3 ноября 2011 г.

randdecimal

Понадобилось написать функцию которая будет отдавать случайный Decimal между значениями a и b (аналогично для встроенной random.randint, только с поддержкой Decimal). Получилось все довольно просто:

from decimal import Decimal
from random import randint


def randdecimal(a, b):
    """
    Return a random decimal N such that a <= N <= b.
    """
    a, b = Decimal(a), Decimal(b)
    diff = b - a

    multiplier = pow(10, abs(diff.as_tuple()[2]))
    diff_to_int = int(diff * multiplier)

    return a + Decimal(randint(0, diff_to_int)) / multiplier

Результат работы:

In [3]: from randdecimal import randdecimal

In [4]: randdecimal(0, 10)
Out[4]: Decimal('5')

In [5]: randdecimal(0, 10)
Out[5]: Decimal('1')

In [6]: randdecimal(0, 10)
Out[6]: Decimal('1')

In [7]: randdecimal('0.13', '0.20')
Out[7]: Decimal('0.20')

In [8]: randdecimal('0.13', '0.20')
Out[8]: Decimal('0.14')

In [9]: randdecimal('0.13', '0.20')
Out[9]: Decimal('0.15')

In [10]: randdecimal('0.135', '0.2')
Out[10]: Decimal('0.139')

In [11]: randdecimal('0.135', '0.2')
Out[11]: Decimal('0.194')

In [12]: randdecimal('0.135', '0.2')
Out[12]: Decimal('0.172')

In [13]: randdecimal('0.135', '0.2')
Out[13]: Decimal('0.182')

In [14]: from decimal import Decimal

In [15]: randdecimal(Decimal('5.34'), 10)
Out[15]: Decimal('8.69')

In [16]: randdecimal(Decimal('5.34'), 10)
Out[16]: Decimal('9.17')

In [17]: randdecimal(Decimal('5.34'), 10)
Out[17]: Decimal('6.34')

In [18]: randdecimal(0, Decimal('5.33'))
Out[18]: Decimal('2.66')

In [19]: randdecimal(0, Decimal('5.33'))
Out[19]: Decimal('2')

In [20]: randdecimal(0, Decimal('5.33'))
Out[20]: Decimal('3.21')

Полностью randdecimal.py вместе с тестами доступен как gist на GitHub'е.

30 октября 2011 г.

Соблюдаем порядок секций при работе с конфиг файлом

Столкнулся с весьма забавной проблемой при работе с конфиг файлами, используя стандартную библиотеку ConfigParser. Дело в том, что до версии 2.7 эта библиотека не соблюдает порядок в котором расположены секции в конфиг файле, потому что хранит их в обыкновенном dict объекте. В версии же 2.7 библиотека использует добавленный в collections класс OrderedDict и никаких проблем с порядком нет.

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

Решение весьма простое, использовать dict_type при инициализации парсера, например так:

from ConfigParser import SafeConfigParser

from django.utils.datastructures import SortedDict


config = SafeConfigParser(dict_type=SortedDict)

Примечание: Я использовал здесь Django'вский SortedDict, но вы можете спокойно использовать свой объект, который соблюдает порядок значений при работе со словарем данных.

Единственный недостаток метода, что он будет работать только для Python 2.6, потому что поддержка dict_type появилась именно в этой версии. Для владельцев же более ранних версий возможно полезным будет узнать о неплохой альтернативе ConfigObj, в которой нет проблем с порядком данных и еще эта библиотека понимает комментарии в конфиг файлах :)

5 октября 2011 г.

django.dispatch.receiver - FTW!

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

from django.contrib.auth.models import User
from django.db.models import signals

...

signals.post_save.connect(auto_create_user_profile, sender=User)

Однако с выходом Django 1.3 ситуация поменялась. Сейчас достаточно задекорировать функцию сигнала, в нашем случае auto_create_user_profile, с помощью @receiver декоратора:

from django.contrib.auth.models import User
from django.dispatch import receiver

...

@receiver(signals.post_save, sender=User)
def auto_create_user_profile(instnance, **kwargs):
    ...

И все, готово! Согласитесь, удобней и легче чем раньше.

зы. Удачного рефакторинга! ;)