Skip to content

Explain our principles on the front page. #422

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 124 additions & 33 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,66 +4,157 @@

# A set of libraries for pluggable business logic components

The dry-python project enforce clean architecture and domain-driven
design in your application.

The easiest way to make your code simple to reason about.

dry-python is a collection of libraries aimed to help you to build an application in your domain.

## Stories
## Principles

- [Express business rules with pure-python objects](#express-business-rules-with-pure-python-objects)
- [Express business scenarios with DSL](#express-business-scenarios-with-dsl)
- [Read business objects directly from multiple data sources](#read-business-objects-directly-from-multiple-data-sources)
- [Build a composition of these objects without boilerplate](#build-a-composition-of-these-objects-without-boilerplate)
- [Use business logic as a library in your web application stack](#use-business-logic-as-a-library-in-your-web-application-stack)

## Express business rules with pure-python objects

You can define business logic processes with clear DSL.
Write small isolated testable classes with segregated interface.
Express your business rules in without coupling to any library related
to HTTP transport layer, ORM database layer, and even our set of
tools. From [Clean
Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
point of view this will be your **entities** layer. From
[Domain-driven
design](https://en.wikipedia.org/wiki/Domain-driven_design)
perspective this will be your **domain business rules** layer.

```pycon

>>> from stories import story, arguments
>>> from attr import attrs, attrib

>>> @attrs
... class Buyer:
... user_id = attrib()
... balance = attrib()
...
... def is_able_to_purchase(self, cost):
... return self.balance > cost

>>> @attrs
... class Order:
... order_id = attrib()
... items = attrib()
...
... def could_be_processed(self):
... return all(item.is_available for item in self.items)

```

>>> class BuySubscription:
## Express business scenarios with DSL

Write your business process with expressive language.

```pycon

>>> from attr.validators import is_callable
>>> from stories import story, arguments, Success, Failure

>>> @attrs
... class Purchase:
... @story
... @arguments("price_id", "user")
... def buy(I):
... I.find_price
... @arguments("user_id", "order_id")
... def make(I):
... I.find_buyer
... I.find_invoice
... I.check_balance
... I.check_availability
... I.persist_payment
... I.persist_subscription
... I.send_subscription_notification
... I.show_category
... I.persist_purchase
... I.send_notifications
...
... # Steps.
...
... def find_buyer(self, ctx):
... ctx.buyer = self.load_buyer(self.user_id)
... return Success()
...
... def find_invoice(self, ctx):
... ctx.order = self.load_order(self.order_id)
... return Success()
...
... def check_balance(self, ctx):
... if not ctx.buyer.is_able_to_purchase(ctx.order.cost):
... return Failure("low balance")
... return Success()
...
... def check_availability(self, ctx):
... if not ctx.order.could_be_processed():
... return Failure("is not available")
... return Success()
...
... def persist_payment(self, ctx):
... ...
...
... # Dependencies.
...
... load_buyer = attrib(validator=is_callable)
... load_order = attrib(validator=is_callable)

```

## Dependencies
## Read business objects directly from multiple data sources

```pycon

>>> from mappers import Mapper
>>> from project.db.models import UserTable, OrderTable

You can integrate it into frameworks you already use.
>>> mapper = Mapper(Buyer, UserTable, {"user_id": "pk"})

>>> @mapper.reader.of(Buyer)
... def load_buyer(user_id):
... return UserTable.objects.filter(pk=user_id)

>>> mapper = Mapper(Order, OrderTable, {"order_id": "pk"})

>>> @mapper.reader.of(Order)
... def load_order(order_id):
... return OrderTable.objects.filter(pk=order_id)

```

## Build a composition of these objects without boilerplate

```pycon

>>> from dependencies import Injector, operation, this
>>> from dependencies.contrib.django import view
>>> from django.http import HttpResponse
>>> from django.shortcuts import redirect
>>> from dependencies import Injector

>>> @view
... class BuySubscriptionView(Injector):
... buy_subscription = workflows.BuySubscription.buy
... price_id = this.kwargs["id"]
...
... @operation
... def post(buy_subscription, price_id, user):
... result = buy_subscription.run(price_id, user)
... if result.is_success:
... return redirect(result.value)
... elif result.failed_on("check_balance"):
... return HttpResponse("<h1>Error: not enough money</h1>")
>>> class MakePurchase(Injector):
... purchase = Purchase
... load_buyer = load_buyer
... load_order = load_order

```

And a framework of your choice will not even notice the change.
## Use business logic as a library in your web application stack

```pycon

>>> from app.views import BuySubscriptionView
>>> from django.views.generic import FormView
>>> from django.urls import reverse_lazy
>>> from project.web.forms import PurchaseForm

>>> urlpatterns = [
... path("buy_subscription/<int:id>/", BuySubscriptionView.as_view()),
... ]
>>> class MakePurchaseView(FormView):
... template_name = "make_purchase.html"
... form_class = PurchaseForm
... success_url = reverse_lazy("complete-purchase")
...
... def form_valid(self, form):
... MakePurchase.purchase.make(user_id=form.user_id, order_id=form.order_id)
... return super().form_valid(form)

```

Expand Down
13 changes: 6 additions & 7 deletions mddoctest.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import builtins
import sys
from doctest import testfile
from glob import glob
from unittest import mock
from sys import exit
from sys import modules
from unittest.mock import Mock


def _setup():
sys.modules["app.views"] = mock.Mock()
builtins.path = mock.Mock()
builtins.workflows = mock.Mock()
modules["project.db.models"] = Mock()
modules["project.web.forms"] = Mock()


def _main():
Expand All @@ -17,7 +16,7 @@ def _main():
for markdown_file in markdown_files:
failed, attempted = testfile(markdown_file, module_relative=False)
exit_code += failed
sys.exit(exit_code)
exit(exit_code)


if __name__ == "__main__":
Expand Down
40 changes: 34 additions & 6 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ mkdocs = "^1.1"
mkdocs-material = "^4.6.3"

[tool.poetry.dev-dependencies]
attrs = "^19.3.0"
dependencies = "^1.0"
django = "^3.0.5"
flake8 = "^3.7.9"
invoke = "^1.4.0"
mappers = "^1.0.2"
pre-commit = "^2.2.0"
stories = "^0.14.0"
yamllint = "^1.21.0"