stories
stories copied to clipboard
Story should return its execution result.
Syntax
~~Once again we would change required syntax to work :sweat_smile:~~
from dataclasses import dataclass
from types import SimpleNamespace
from typing import Callable
from stories import story
from app.repositories import load_order, load_customer, create_payment
@story
def purchase(it, state):
it.find_order(state)
it.find_customer(state)
it.check_balance(state)
it.persist_payment(state)
@dataclass
class Steps:
load_order: Callable
load_customer: Callable
create_payment: Callable
def find_order(self, state):
state.order = self.load_order(state.order_id)
def find_customer(self, state):
state.customer = self.load_customer(state.customer_id)
def check_balance(self, state):
if not state.order.affordable_for(state.customer):
raise Exception
def persist_payment(self, state):
state.payment = self.create_payment(
order_id=state.order_id, customer_id=state.customer_id
)
steps = Steps(
load_order=load_order,
load_customer=load_customer,
create_payment=create_payment,
)
state = SimpleNamespace(order_id=1, customer_id=1)
print(purchase.bind(steps).run(state))
purchase
find_order
find_customer
check_balance
persist_payment
order_id = 1 # Story argument
customer_id = 1 # Story argument
order = Order(product=Product(name='Books'), cost=Cost(amount=7)) # Set by purchase.Steps.find_order
customer = Customer(balance=8) # Set by purchase.Steps.find_customer
payment = Payment(due_date='tomorrow') # Set by purchase.Steps.find_subscription
Result object is...
a restored context representation from stories version 4.0.
Principles
-
Actors: this feature become available just by changing name of the definition from
itto something likebot,developer,adminetc. -
Conditionals: this feature become available just by using
if,elseandmatchkeywords. Access to the state attribute outside of step would be treated as conditional in result representation. It's value still limited to true, false and enum attribute.itdelegate check that step argument is the same as story argument. This pattern would prevent usage of loop inside story definition. -
Typing:
stories.typingwould define bunch of generics and protocols for step, steps, state, etc. So it would be possible to annotate story function with__annotations__future enabled including inner stories. -
Async:
runmethod should not care if story is a function, a generator, or a coroutine. This kind of API do not need an executor in the first place.runmethod usage should be transparent to the caller code (a Django view for example). But we would explicitly test and document every feature with sync and async variants. -
Bound story representation:
repr(purchase)andrepr(purchase.bind())would work the same way as it works in stories 4.0. - Inner stories: TODO
-
Ecosystem integration: we pass result object to the
logger.debugmanually in every place. This is boilerplace code we would write inside every Django view. This logger could be an integration layer for pytest, sentry, django logging, ELK, etc. We would document how to use it with most important cases. -
Replace logger: In some cases users would like to stream business process events as separate log entries using struct log or celery task progress features.
purchase.use(struct_log_adapter).bind().run()to stream every step and assignment to ELK using structlog adapter. This adapter return None, so user readable representation is not printed in ipython.
2023 Aug 8 update
Typing support for conditional inner stories without when and unless.
from attrs import define
from stories import Story, I
@define
class Outer(Story):
I.conditional
def conditional(self, state):
if state.condition:
self.inner(state)
inner: Story