Provides primitives for transactional application logic
Project description
Classic Operations
Библиотека предоставляет примитив для выделения границ операций в приложении.
Введение
Общая проблема, которую эта библиотека решает, это выделение границ операций у приложения и вызов на границах стороннего кода, не имеющего отношения к приложению.
Самый частый пример - использование транзакций в приложении. С одной стороны,
хочется иметь декоратор вроде 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__
, и,
таким образом, произойдет обертывание метода в транзакцию, хотя код сервиса
напрямую ничего о транзакции не упоминает.
Callbacks
Также можно повешать коллбеки на разные точки в жизненном цикле. Коллбеки не должны принимать никаких аргументов.
В примере указаны все возможные коллбеки:
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
, исключение будет выброшено наружу
Динамические callbacks
Объект-операция предоставляет способ добавить коллбеки уже после запуска
самой операции, внутри блока 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
Декоратор 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)
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.