Skip to main content

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)

Отмена операции

Для отмены операции используется исключение 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 добавлен для возможности подавить распространение исключения.

Вывод ID операции в логи

В операцию встроен атрибут "ID операции". При входе в блок with будет вызван __enter__ внутри которого будет сгенерировано значение ID операции (по алгоритму uuid4), при выходе - __exit__, внутри которого значение ID операции будет сброшено (станет равно None).

Сгенерированное ID операции можно добавить для выводов в логи, как указано в примере ниже:

import logging

from classic.components import component
from classic.operations import Operation, operation


# Код приложения
@component
class SomeService:
    logger: logging.Logger

    @operation
    def some_method(self):
        self.logger.info('Some log')


# Настройка логгера, самое главное: 
#   добавление operation_id в формат выводимого сообщения
logging.basicConfig(
    level='INFO',
    format=(
        '%(asctime)s.%(msecs)03d [%(levelname)s]|[%(name)s]'
        '|[%(operation_id)s]: %(message)s'
    ),
    datefmt='%Y-%m-%d %H:%M:%S',
)

logger = logging.getLogger('app')
operation_ = Operation()
service = SomeService(
    operation_=operation_,
    logger=logger,
)

# Добавляем operation_id в выводимую запись 
old_factory = logging.getLogRecordFactory()


def record_factory(*args, **kwargs):
    record = old_factory(*args, **kwargs)
    record.operation_id = operation_.id
    return record


logging.setLogRecordFactory(record_factory)

# Где-то в клиентском коде, например, в адаптерах:
service.some_method()

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

classic_operations-0.3.2.tar.gz (14.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

classic_operations-0.3.2-py3-none-any.whl (10.0 kB view details)

Uploaded Python 3

File details

Details for the file classic_operations-0.3.2.tar.gz.

File metadata

  • Download URL: classic_operations-0.3.2.tar.gz
  • Upload date:
  • Size: 14.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.10.13

File hashes

Hashes for classic_operations-0.3.2.tar.gz
Algorithm Hash digest
SHA256 e21c063c050cc946f419ae50e63e5a8fc52af443ec6326c521117a12d7804eaf
MD5 a9ada8793d40af87c746a3c17f8096a7
BLAKE2b-256 db7c69a068a6d349beacab4bdc83ab1a90b1350c3205b9e85700c1ac992897f8

See more details on using hashes here.

File details

Details for the file classic_operations-0.3.2-py3-none-any.whl.

File metadata

File hashes

Hashes for classic_operations-0.3.2-py3-none-any.whl
Algorithm Hash digest
SHA256 515918199ccc24521d226e88e1f8e1014403f5ae4fd45db055a044e1374b9938
MD5 56163def817bf049ad1e35e01c527996
BLAKE2b-256 6c8869d494dea62ff7ecfe32999558f5e5d3e01708a8fadeac785b654ad82b35

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page