by

Where communities thrive


  • Join over 1.5M+ people
  • Join over 100K+ communities
  • Free without limits
  • Create your own community
People
Repo info
Activity
    Artem Malyshev
    @proofit404
    Можно посмотреть на https://github.com/dry-python/bookshelf
    Тут основной принцип 1 в 1 как в чистой архитектуре Мартина.
    В entities пишем бизнес правила (dataclasses без frameworkов), в usecases пишем бизнес сценарии (stories), реализацию в usecases прокидываем через dependencies.
    FastAPI views выглядят одной строкой, которая обращается к инжектору и передаёт аргументы из запроса в usecase.
    MuslimBeibytuly
    @MuslimBeibytuly
    @proofit404 не нашел документацию о том, как можно тестить отдельный шаг в stories, как воссоздать ctx для функции
    Artem Malyshev
    @proofit404
    Отдельные шаги в story тестировать не надо. Надо тестировать поведение стори целеком.
    Если есть необходимость тестирования отдельного шага - значит скорее всего в нём написана реализация завязанная на особенности системы, а не бизнес логика.
    В таком случае детали реализации лучше вынести в отдельную функцию, протестировать её отдельно от story, и закинуть в инстанс стори уже через dependencies.
    Например:
    1. Шаг в котором нет деталей реализации
    2. Отдельно детали реализации
    3. Добавляем одно в другое через конструктор
    MuslimBeibytuly
    @MuslimBeibytuly
    @proofit404 понял ошибку, можно ли вытащить все аргументы из ctx как dict? хочу засунуть все в kwargs
    Artem Malyshev
    @proofit404
    Это довольно хрупкое решение, которое делает интерфейс вызова неявным. Лучше передавать агрументы явно.
    Понимаю что лень писать, но это экономит время на чтение кода в дальнейшем. Можно, например, отделить keyword only arguments с помощью def foo(a, b, *, c=1, d=2) синтаксиса.
    MuslimBeibytuly
    @MuslimBeibytuly
    @proofit404 в mr получил:
    accepted_data = { key: value for key, value in ctx.__dict__[ '_Context__ns' ].items() }
    Знаю что костыль, но с другой стороны понимаю что лень все 20 аргументов отправлять в сервис руками:c есть ли какой-нибудь в arguments указывать не только название аргумента, но и описание в виде typing аннотации?(pydantic модель, это не по ddd, однако мы не стремимся получить идеальный ddd, лучше понятный по stories, но не слишком раздутый код)
    Artem Malyshev
    @proofit404
    Если поишлёшь примеры кода, который является проблемным, я подумаю как это сделать лаконично.
    Yevhen Tsybulskyi
    @Hammer2900
    я написал в личку, в общий чат будет немного не то скидывать код
    Artem Malyshev
    @proofit404
    @MuslimBeibytuly your approach will not work from stories v0.11.2, pin your requirements or rewrite this hack.
    I hope it isn't across all over your codebase :)
    paveloder
    @paveloder

    Возможно ли использовать type hinting для контекста при наличии контракта, например:

    class Story:
        @story
        def do(I):
    
            I.one
    
        def one(self, ctx: Context):
    
            return Success(bar=ctx.foo + "2")
    
    @Story.do.contract
    class Context(BaseModel):
    
        foo: str
        bar: str

    как это правильно реализовать?

    Artem Malyshev
    @proofit404
    Привет. Да, type hinting можно использовать для аннотаций контекста.
    Есть пара идей для плагина для mypy чтобы автоматизировать некоторые шаблонные куски кода. Но в целом должно работать.
    Artem Malyshev
    @proofit404
    После того, как смержим dry-python/stories#271 должно быть ещё лучше.
    ramidk
    @ramidk
    @proofit404 Добрый день! А почему нет возможности получить код ошибки из Failure()? Например: result.failure_reason
    Artem Malyshev
    @proofit404
    @ramidk А для каких целей он вам нужен? Если по ключу из dict достать или использовать в result.reason is Enun.foo, то это не получится. Для каждой композиции story собирается новый Enum.
    Artem Malyshev
    @proofit404
    Потому что композиция может состоять из разных story.
    Yevhen Tsybulskyi
    @Hammer2900
    вопрос такой, бывают такие случаи логики где применяются много if логики и она в таких случаях бывает заковыристая, как правильно оформлять стори в этом случае ?
    image.png
    вот маленький пример
    нужно сделать много стори и соединить их ? или же можно как то переключать раут в стори на другой шаг ?
    Artem Malyshev
    @proofit404
    Лучше сделать из этого одну плоскую историю. Каждый шаг в порядке приоритетности проверки условий.
    Если шаг проверяет своё условие и понимет, что ему ничего делать ненужно - он возвращает Success. Обработка переходит к следующему шагу.
    Eсли шаг понимает что условие удовлетворено - вызвает необходимую обработку, возвращает Skip. Обработки данный истории прекращается.
    Eсли обратобка условия это что-то сложное, лучше оформить саму обработку в отдельную историю в отдельном классе.
    А вот уже шаг проверки условия преобразовать во вложенную историю из двух шагов. Первый проверяет условию, второй - история из отдельного класса.
    Yevhen Tsybulskyi
    @Hammer2900
    хорошо, спасибо, попробую, надеюсь это будет выглядеть красиво. Конечно бы пример в документации не помешал бы по этому поводу...
    Artem Malyshev
    @proofit404
    Уже в процессе :)
    ramidk
    @ramidk

    @proofit404 Добрый день! возможно ли (чтобы убрать лишний step вызова реализации) вместо:

    >>> def find_price(self, ctx):
    ...     ctx.price = self.impl.find_price(ctx.price_id)
    ...     return Success()
    
    >>> def __init__(self, impl):
    ...     self.impl = impl

    Использовать:

    >>> class Price:
    ...
    ...     @story
    ...     @arguments("category", "price_id")
    ...     def find_price(I):
    ...
    ...         I...
    ...         I...
    
    
    >>> class Subscription(MethodDefinitions):
    ...
    ...     @story
    ...     @arguments("category_id", "price_id", "profile_id")
    ...     def buy(I):
    ...
    ...         I.find_category
    ...         I.impl.find_price
    ...
    ...        def __init__(self, impl):
    ...            self.impl = impl
    
    Subscription(impl=Price()).buy
    Artem Malyshev
    @proofit404
    Привет. Можно написать базовый класс Methods, в котором определить __getattr__ метод. Тогда можно будет возвращать необходимые шаги на лету. Но тут непонятно по какому принципу определять имена переменных, которые нужно присвоить в контекст.
    ramidk
    @ramidk
    Спасибо, да насчет контекста - согласен)
    Artem Malyshev
    @proofit404
    Можно передавать контекст целиком в метод реализации, и присваивать необходимые значения уже там внутри. Такой код будет так же тяжело понять как например пачку mixin, например в django views generic. Посмотреть в самом контексте кто и что туда положил можно через print(ctx). Но в коде будет грязь.
    ramidk
    @ramidk
    спасибо за ответ)
    ramidk
    @ramidk
    @dataclass
    class TokenUseCase:
        @story
        @arguments('user')
        def obtain_token(I):
            I.require_user_active
            I.create_token
            I.return_token
    
        def require_user_active(self, ctx):
            if ctx.user.is_active:
                return Success()
            return Failure()
    
        def create_token(self, ctx):
            ctx.token = self.create_user_token(ctx.user)
            return Success()
    
        def return_token(self, ctx):
            return Result({'token': ctx.token})
    
        # deps
        create_user_token: Callable

    @proofit404 Добрый вечер! Например у меня есть TokenUseCase, который я хочу использовать напрямую для получения токена.

    result = TokenUseCase(create_user_token=create_user_token).obtain_token.run(user=user)
    token = result.value['token']

    Но также я хочу переиспользовать TokenUseCase.obtain_token как sub-story.
    Но так сделать не получается, т.к. obtain_token возвращает Result и прекращает выполнение OtherUseCase.outer_story

    obtain_token = TokenUseCase(create_user_token=create_user_token).obtain_token
    OtherUseCase(find_token=obtain_token).outer_story.run()

    Как правильно организовать код в таком случае? Заранее спасибо!

    Artem Malyshev
    @proofit404

    Привет. Сразу скажу что красивого решения я пока не нашёл.
    Я обычно такое поведение выношу в отдельную story, т.е. Result возвращаю на верхнем уровне вложенности.

    @dataclass
    class TokenUseCase:
        @story
        @arguments('user')
        def obtain_user_token(I):
            I.obtain_token
            I.return_token
    
        @story
        @arguments('user')
        def obtain_token(I):
            I.require_user_active
            I.create_token

    Таким образом мы можем вызвать obtain_user_token как самостоятельную сторю, а obtain_token как дочернюю сторю для какой-то более большой.

    Artem Malyshev
    @proofit404
    Можем обсудить более удобное решение тут dry-python/stories#352
    ramidk
    @ramidk
    Да, спасибо, ответил в треде
    ramidk
    @ramidk
    @proofit404 а как работать с транзакциями бд? Например нужно частичный rollback сделать в ходе выполнения story. Если импортировать from django.db import transaction и использовать transaction внутри story то опять возвращаемся к vendor lock. Спасибо.
    ramidk
    @ramidk
    @proofit404 Добрый день! Еще вопрос, есть ли в планах сделать для stories какую-то обертку для io операций (чтобы например обернуть вызов к бд). Например как в библиотеке returns. Или в проектах вы используете returns? Но вроде не хочется couple stories и returns =) Буду благодарен за ответ.
    Artem Malyshev
    @proofit404
    Привет. Если нужно делать rollback на основании какой-то логики, то можно transaction.enter и transaction.exit пробросить через dependencies. Тогда внутри story не будет импортов из Django.
    Я не очень понимаю как должен вести себя этот io wrapper внутри story. Какова его цель?
    ramidk
    @ramidk

    Что-то вроде такого:

    ```
    # step
    def load_image(self, ctx):
        result = io_call(ctx.get_image_file(image_id=ctx.image_id))
        if not is_successful(result):
            return Failure('image_cannot_be_loaded')
        ctx.image = unwrap(result)
        return Success()
    ```

    С целью обозначить что будет выполнена io операция, которая которая может вызвать исключение, и чтобы можно было это исключение транслировать на зарегистрированную Failure.

    Artem Malyshev
    @proofit404
    Похожий враппер, транслирующий эксепшены в результат скорее всего напишем, но привязывать что это IO скорее всего не будем.
    Позожий механизм может быть и для mappers нужен, поэтому не факт что враппер станет частью stories.
    ramidk
    @ramidk
    И в идеале наверное, чтобы был какой-то маппер, для для более гранулярного сопоставления исключений с failures.
    А необработанные летели наверх или маппились с дефолтным  failure. Имхо))
    ```
    # step
    def load_image(self, ctx):
        wrapped = io_call(ctx.get_image_file(image_id=ctx.image_id))
        if not is_successful(wrapped):
            return Failure(wrapped.failure)
        ctx.image = unwrap(wrapped)
        return Success()
    
    class RegisteredFailures(Enum):
        image_cannot_be_loaded = auto()
        image_not_found = auto()
        permission_denied = auto()
    
    def io_call(): -> FailureResult
        pass
    
    class ImageFailureResult(FailureResult):
        @property
        def failure(self):
            failure = self._map(self._exception, RegisteredFailures.image_cannot_be_loaded)
            return failure
    ```
    ramidk
    @ramidk

    Позожий механизм может быть и для mappers нужен, поэтому не факт что враппер станет частью stories.

    Да, понятно. Было бы здорово, если бы он был хорошо совместим со stories.

    Artem Malyshev
    @proofit404
    По идее один failure reason должен быть привязан к одному шагу истории, а гасить exception нужно на уровне слоя реализации.
    Тут точно нужен mapping exception в failure?
    ramidk
    @ramidk

    По идее один failure reason должен быть привязан к одному шагу истории, а гасить exception нужно на уровне слоя реализации.

    @proofit404 да, в таком случае маппинг и не нужен видимо. Во всяком случае, сейчас не вижу когда может пригодиться.

    ramidk
    @ramidk
    Еще вопрос) В сторе у нас failures летят наверх (на самую верхнюю story), а как в таком случае делать разделение на слои?
    Например: Мы хотим авторизовать пользователя, в нижней сторе не удается создать токен, и эта нижняя сторя возвращает token_creation_falied. И хотелось бы на слое пользователя преобразовывать token_creation_falied в user_login_failed - т.е. в ошибку этого слоя. Возможно я что-то не улавливаю в этой концепции)
    Artem Malyshev
    @proofit404
    Я думаю такая конверсия нужна, только если у тебя по DDD появляются какие-то сложные вложенные друг в друга домены.
    Я с настолько сложными концепциями не работал. У меня всегда failure возвращает настоящую ошибку по определению предметной области.
    Можно делать что-то типа return Failure(self.convert('low-level-reason')) и этот convert прокидывать через DI. Тогда можно будет написать свой convert для разных доменов.
    ramidk
    @ramidk
    Артем, спасибо!