Буду вести разработку более прозрачно, начну публиковать небольшие дневники. slonogram, в первую очередь, должен быть расширяем и предлагать нечто лучше, чем существующие библиотеки, поэтому следует начать с основ. At core, библиотека для ботов это несложно - датаклассы и запросы к API. Сложная часть это обработка событий, она должна быть продумана так, чтобы хватало всем - и среднему разработчику ботов с знаниями примерно никакими, и любителю долбится в гланды. Потому начнем с основ - с библиотеки для обработки событий.
За основу возьмем следующий подход:
C = TypeVar("C")
I = TypeVar("I")
N = TypeVar("N")
O = TypeVar("O")
class Handler(Protocol[C, I, N, O]):
def __call__(self, ctx: C, req: I, next: N, /) -> O:
...
У неподготовленного читателя может случится приступ "а зачем?", поясню каждый генерик здесь:
C
- контекст. Контекст, в котором обработчик выполняется. Может содержать в себе какие-нибудь причуды для DI, например - динамический лексический скоупинг (уже знакомый вам dishka, по сути, является реификацией лексического скоупинга, без древовидных скоупов)I
- входной тип для функции.N
- функция-продолжение. Почему это генерик - будет объяснено когда приступим к типизации.O
- тип результата функции.
Больше всего люди задаются вопросами, когда видят определение Handler
как функции с продолжением. Вспомним где гипотетический читатель мог видеть такой паттерн - да это ж middleware!
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
start_time = time.perf_counter() # <- Pre-process
response = await call_next(request) # <- Call continuation
# ------------------------------------- <- Post-process
process_time = time.perf_counter() - start_time
response.headers["X-Process-Time"] = str(process_time)
return response
# Разработчики уже все за меня расписали
def simple_middleware(get_response):
# One-time configuration and initialization.
def middleware(request):
# Code to be executed for each request before
# the view (and later middleware) are called.
response = get_response(request)
# Code to be executed for each request/response after
# the view is called.
return response
return middleware
Зачем же смешивать понятие middleware и актуальной функции-обработчика? Но, на самом деле, никто ничего не смешивает, просто в веб-разработке, так уж исторически сложилось, принято делить middleware и актуальную функцию-обработчик. Мы так не будем делать, но, если вам это важно, такая техника называется delimited continuations:
In programming languages, a delimited continuation, composable continuation or partial continuation, is a "slice" of a continuation frame that has been reified into a function. Unlike regular continuations, delimited continuations return a value, and thus may be reused and composed.
Плюсы написаны - такие функции можно между собой "соединять". Поскольку на типизированном питоне делать описанное то еще развлечение, покажу пример на untyped, а дальше уже подумаем как можно типизировать такое чудо:
# При вызове возвращает просто аргумент, конец цепочки
def end(ctx, arg, next):
return arg
# "Упасть" по цепочке вниз.
# Занятно! - then(fallthrough, fallthrough) = бесконечная рекурсия
def fallthrough(ctx, arg, next):
return next(ctx, arg, fallthrough)
# Добавить к аргументу
def add(delta):
def inner(ctx, arg, next):
return next(ctx, arg + delta, fallthrough)
return inner
# Соединить две функции
def then(lhs, rhs):
def inner(ctx, arg, chain_rest):
def cont(ctx, arg, rhs_next):
return rhs(ctx, arg, then(rhs_next, chain_rest))
return lhs(ctx, arg, cont)
return inner
Бэм! Попользуемся этим:
ctx = {}
add2 = then(add(1), add(1))
print(add2(ctx, 0, end))
# Output: 2
Если вам показалось определение then
непонятным, то не беспокойтесь - вы в этом не одиноки. Давайте поясню:
По определению lhs
вызовет cont
, rhs
вызовет then(rhs_next, chain_rest)
, а вот что это? Выглядит же стремно! Тут логика проста, но рекурсивна, если then соединяет две функции, то все, что нам нужно это соединить rhs_next
(продолжение, переданное в rhs) с chain_rest
(остальная часть цепочки). А если так?
def f(ctx, arg, next):
return next(ctx, arg + 1, lambda ctx, arg, next: next(ctx, str(arg), fallthrough))
heh = then(f, add(1))
print(repr(heh(ctx, 0, end)))
Ответ - '2', заметим, что порядок исполнения у нас такой
f -> (add(1) -> add(1)) -> str(result)
Схема
f
+------------+
| +--------+ |
| | add(1) | |
| +--------+ |
| | |
| +--------+ |
| | add(1) | |
| +--------+ |
+------------+
|
str
А мы еще не использовали контекст. А давайте
# di add!!
def diad(ctx, arg, next):
return next(ctx, arg + ctx['delta'], fallthrough)
heh = then(diad, diad)
print(heh({'delta': 1}, 0, end))
# Output: 2
Типизировать это все - занятие веселое! В следующих сериях.