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
    Отдельные шаги в 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
    Артем, спасибо!
    Artem Malyshev
    @proofit404

    These chat rooms left for historical purposes only. All active maintainers of the project have left gitter.

    Projects are still properly maintained. If you have any questions, please file an issue in our issue tracker:

    Issues are better to keep track of the community interests. It could be converted to tasks easily. It preserves the history of resolution. It's google friendly. Let's keep our conversation in a structured way! See you there.

    David Stone
    @dcstone09
    thanks