Библиотека предоставляет примитив для выделения границ операций в приложении.
Общая проблема, которую эта библиотека решает, это выделение границ операций у приложения и вызов на границах стороннего кода, не имеющего отношения к приложению.
Самый частый пример - использование транзакций в приложении. С одной стороны,
хочется иметь декоратор вроде transactional
, которым можно было бы просто
обернуть метод в приложении, с другой стороны, хотелось бы, чтобы приложение
не упоминало транзакции, так как это все-так больше имеет отношение к БД.
Объект-операция представляет собой контейнер, в которой можно положить контекстные менеджеры, и/или коллбеки. Затем объект операцию можно стартовать, при старте отработают контекстные менеджеры и коллбеки, затем, при завершении, соответствующие коллбеки и закроются контекстные менеджеры.
Пример:
from classic.operations import Operation, operation
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
# Код приложения
class SomeService:
# Dependency Injection
def __init__(self, operation_: Operation):
self.operation_ = operation_
def some_method(self):
with self.operation_:
print('Здесь должна происходить полезная работа')
@operation
def another_method(self):
"""Этот метод полностью аналогичен some_method.
Декоратор operation внутри обращается к self.operation_,
и так же помещает в блок with. Сделано ради сахара.
"""
print('Здесь должна происходить полезная работа')
# Композит
engine = create_engine('sqlite://')
session = Session(engine)
operation_ = Operation(context_managers=[session])
service = SomeService(operation_=operation_)
# Где-то в клиентском коде, например, в адаптерах:
service.some_method()
В примере каждый вызов some_method будет запуском операции. При входе в блок
with будет вызван __enter__
у сессии, при выходе - __exit__
, и,
таким образом, произойдет обертывание метода в транзакцию, хотя код сервиса
напрямую ничего о транзакции не упоминает.
Также можно повешать коллбеки на разные точки в жизненном цикле. Коллбеки не должны принимать никаких аргументов.
В примере указаны все возможные коллбеки:
from functools import partial
from classic.operations import Operation
lazy_print = partial(print)
operation_ = Operation(
before_start=[lazy_print('Попытка начать операцию')],
after_start=[lazy_print('Операция начата')],
before_complete=[lazy_print('Операция успешно подходит к завершению')],
after_complete=[lazy_print('Операция успешно завершена')],
on_cancel=[lazy_print('Операция отменена')],
on_finish=[lazy_print('Это вызовется после завершения '
'операции в любом случае')],
)
Порядок вызова при входе в блок with
(__enter__
):
-
before_start
. Если хотя бы один из них вызовет исключение, исполнение прервется, будут вызваныon_cancel
иon_finish
, исключение будет выброшено наружу. -
__enter__
у всех контекстных менеджеров, переданных в операцию. Если хотя бы из них вызовет исключение, исполнение прервется, будут вызваныon_cancel
иon_finish
, исключение будет выброшено наружу. -
after_start
. Если хотя бы один из них вызовет исключение, исполнение прервется, будут вызваныon_cancel
иon_finish
, исключение будет выброшено наружу.
Порядок вызова при выходе из блока with
(__exit__
):
-
before_complete
. Если хотя бы один из них вызовет исключение, исполнение не прервется сразу, так как необходимо попытаться вызвать__exit__
у всех контекстных менеджеров из операции. Исключение будет отложено, но на следующем шаге оно будет передано в__exit__
каждому контекстному менеджеру. -
__exit__
у всех контекстных менеджеров, переданных в операцию. Если хотя бы один из них вызовет исключение, исполнение не должно прерваться, сразу, все методы__exit__
у всех вложенных контекстных менеджеров должны быть вызваны.Если при этом произошло исключение, или есть исключение, отложенное с предыдущего шага, то исполнение прервется, будут вызваны
on_cancel
иon_finish
, исключение будет выброшено наружу. -
after_complete
. Если хотя бы из них вызовет исключение, исполнение прервется, будут вызваныon_cancel
иon_finish
, исключение будет выброшено наружу
Объект-операция предоставляет способ добавить коллбеки уже после запуска
самой операции, внутри блока with
. Такие коллбеки будут одноразовыми,
объект-операция забудет о них после завершения текущей операции.
Вне запущенный операции эти методы вызывать нельзя, они будут генерировать
исключение AssertionError
Пример:
from classic.operations import Operation, operation
class SomeService:
# Dependency Injection
def __init__(self, operation_: Operation):
self.operation_ = operation_
@operation
def some_method(self):
self.operation_.after_complete(
lambda: print('Операция завершена успешно')
)
print('Здесь должна происходить полезная работа')
@operation
def another_method(self):
print('Еще один очень полезный метод')
service = SomeService(operation_=Operation())
# Выведет сначала "Здесь должна происходить полезная работа",
# затем "Операция завершена успешно"
service.some_method()
# Выведет только "Еще один очень полезный метод"
service.another_method()
В операцию встроен счетчик вызовов. Повторный вход в блок with
с операцией
после входа не вызовет прогон коллбеков заново:
from classic.operations import Operation, operation
class SomeService:
# Dependency Injection
def __init__(self, operation_: Operation):
self.operation_ = operation_
@operation
def some_method(self):
self.another_method()
@operation
def another_method(self):
print('Еще один очень полезный метод')
service = SomeService(operation_=Operation())
# Второй вызов operation в этой операции будет пропущен.
service.some_method()
Объект-операция построен на базе threading.local
, его можно использовать из
разных потоков.
Декоратор operation сделан ради "сахара". Его применение позволяет не писать весь код метода на один tab справа, и, вероятно, покроет 95% случаев использования библиотеки.
Кроме того, декоратор использует extra_annotations
из classic.components, из-за
этого, при применении декоратора operation
с components, можно не прописывать
Operation
в зависимостях:
from classic.components import component
from classic.operations import operation
@component
class SomeService:
@operation
def some_method(self):
print('95% кейс)))')
Также можно указать, какое поле у self
будет использовать декоратор:
from classic.operations import operation
class SomeService:
def __init__(self, read, write, source_repo, target_repo):
self.read = read
self.write = write
self.source_repo = source_repo
self.target_repo = target_repo
@operation('read')
def some_method(self):
# Представьте себе, что здесь происходит обращение к одной БД,
# проверка и преобразование данных, и запись одной транзакцией в другую
some_objects = self.source_repo.load_objects()
with self.write:
self.target_repo.write(some_objects)
Для отмены операции используется исключение Cancel:
from classic.components import component
from classic.operations import operation, Cancel, Operation
@component
class SomeService:
@operation
def plain_usage(self):
# после этого исключения class Operation произведет отмену операции,
# при этом исключение Cancel будет выброшено наружу
raise Cancel
@operation
def plain_usage_with_suppress(self):
# Если установить suppress=True, так же произойдет отмена,
# но исключение НЕ БУДЕТ выброшено наружу
raise Cancel(suppress=True)
@operation
def class_usage(self):
# добавлено ради сахара, ведет себя точно так же
raise Operation.Cancel
@operation
def decorator_usage(self):
# добавлено ради сахара, ведет себя точно так же
raise operation.Cancel
Строго говоря, для отмены операции можно использовать любое исключение. Cancel добавлен для возможности подавить распространение исключения.