Package: classic-operations
Classic OperationsБиблиотека предоставляет примитив для выделения границ операций в приложении.ВведениеОбщая проблема, которую эта библиотека решает, это выделение границ операций
у приложения и вызов на границах стороннего кода, не имеющего отношения
к приложению.Самый частый пример - использование транзакций в приложении. С одной стороны,
хочется иметь декоратор вродеtransactional, которым можно было бы просто
обернуть метод в приложении, с другой стороны, хотелось бы, чтобы приложение
не упоминало транзакции, так как это все-так больше имеет отношение к БД.Объект-операция представляет собой контейнер, в которой можно положить
контекстные менеджеры, и/или коллбеки. Затем объект операцию можно стартовать,
при старте отработают контекстные менеджеры и коллбеки, затем, при завершении,
соответствующие коллбеки и закроются контекстные менеджеры.Пример:fromclassic.operationsimportOperation,operationfromsqlalchemyimportcreate_enginefromsqlalchemy.ormimportSession# Код приложенияclassSomeService:# Dependency Injectiondef__init__(self,operation_:Operation):self.operation_=operation_defsome_method(self):withself.operation_:print('Здесь должна происходить полезная работа')@operationdefanother_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Также можно повешать коллбеки на разные точки в жизненном цикле.
Коллбеки не должны принимать никаких аргументов.В примере указаны все возможные коллбеки:fromfunctoolsimportpartialfromclassic.operationsimportOperationlazy_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Пример:fromclassic.operationsimportOperation,operationclassSomeService:# Dependency Injectiondef__init__(self,operation_:Operation):self.operation_=operation_@operationdefsome_method(self):self.operation_.after_complete(lambda:print('Операция завершена успешно'))print('Здесь должна происходить полезная работа')@operationdefanother_method(self):print('Еще один очень полезный метод')service=SomeService(operation_=Operation())# Выведет сначала "Здесь должна происходить полезная работа",# затем "Операция завершена успешно"service.some_method()# Выведет только "Еще один очень полезный метод"service.another_method()Счетчик вызововВ операцию встроен счетчик вызовов. Повторный вход в блокwithс операцией
после входа не вызовет прогон коллбеков заново:fromclassic.operationsimportOperation,operationclassSomeService:# Dependency Injectiondef__init__(self,operation_:Operation):self.operation_=operation_@operationdefsome_method(self):self.another_method()@operationdefanother_method(self):print('Еще один очень полезный метод')service=SomeService(operation_=Operation())# Второй вызов operation в этой операции будет пропущен.service.some_method()ПотокобезопасностьОбъект-операция построен на базеthreading.local, его можно использовать из
разных потоков.Декоратор operationДекоратор operation сделан ради "сахара". Его применение позволяет не писать
весь код метода на один tab справа, и, вероятно, покроет 95% случаев
использования библиотеки.Кроме того, декоратор используетextra_annotationsиз classic.components, из-за
этого, при применении декоратораoperationс components, можно не прописыватьOperationв зависимостях:fromclassic.componentsimportcomponentfromclassic.operationsimportoperation@componentclassSomeService:@operationdefsome_method(self):print('95% кейс)))')Также можно указать, какое поле уselfбудет использовать декоратор:fromclassic.operationsimportoperationclassSomeService:def__init__(self,read,write,source_repo,target_repo):self.read=readself.write=writeself.source_repo=source_repoself.target_repo=target_repo@operation('read')defsome_method(self):# Представьте себе, что здесь происходит обращение к одной БД,# проверка и преобразование данных, и запись одной транзакцией в другуюsome_objects=self.source_repo.load_objects()withself.write:self.target_repo.write(some_objects)