doctype html html head meta(charset="utf-8") meta(name="description" content="Introducing dry-python") meta(name="author" content="Artem Malyshev") meta(name="apple-mobile-web-app-capable" content="yes") meta(name="apple-mobile-web-app-status-bar-style" content="black-translucent") meta(name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no") title Introducing dry-python body .reveal .slides section h2 Introducing h1 a(href="https://dry-python.org/"). dry-python h4 Artem Malyshev section h2 BIO ul li Co-Founder at #[a(href="https://drylabs.io/") drylabs.io] li #[a(href="https://dry-python.org/") dry-python.org] li Django Channels 1.0 li 5 years of experience in Python section h2 Code is... p.fragment b hard p.fragment b and frustrating p.fragment Let's consider we're developing subscription button for a web service section(data-background-image=require("./pics/startup.jpg").default data-background-size="contain") h2(style="color: white; background-color: black") Startup br br br br br br br br section h2 Micro framework ol li.fragment Long handlers li.fragment Lots of "if" statements section h2 Long handlers pre code.python. 85 @app.route('/subscriptions/') 86 def buy_subscription(page): ... 121 if props[-1].endswith('$'): 122 -#{">"} props[-1] = props[-1][:-1] 123 pre.fragment code.python. Traceback (most recent call last): File "views.py", line 1027, in buy_subscription ZeroDivisionError: division by zero section(data-background-image=require("./pics/enterprise.jpg").default data-background-size="contain") h2(style="color: white; background-color: black") Enterprise br br br br br br br br section h2 Big framework ol li.fragment You need method flowchart li.fragment Zig-zag in the traceback li.fragment Framework internals leak section h2 Implicit API pre code.python. class SubscriptionViewSet(viewsets.ModelViewSet): queryset = Subscription.objects.all() serializer_class = SubscriptionSerializer permission_classes = (CanSubscribe,) filter_class = SubscriptionFilter ol li.fragment What exactly does this class do? li.fragment How to use it? section(data-background-image=require("./pics/method-flowchart.png").default data-background-size="contain") br section h2 Framework internals leak pre code.python. class SubscriptionSerializer(Serializer): category_id = IntegerField() price_id = IntegerField() pre.fragment code.python. def recreate_nested_writable_fields(self, instance): for field, values in self.writable_fields_to_recreate(): related_manager = getattr(instance, field) related_manager.all().delete() for data in values: obj = related_manager.model.objects.create( to=instance, **data) related_manager.add(obj) section h2 As a result code is... ol li.fragment Fragile li.fragment Hard to reason about li.fragment Time-consuming section img(src=require("./pics/crazy-telephone-wires.png").default) section blockquote p If your code is crap, stickies on the wall won't help. a(href="https://twitter.com/henrikkniberg"). @HenrikKniberg img(src=require("./pics/stickers-on-the-wall.jpg").default) section h2 Service layer p Defines an application's boundary with a layer of services that establishes a set of available operations and coordinates the application's response in each operation. p b by Randy Stafford section h2 Business objects pre code.python. def buy_subscription(category_id, price_id, user): category = find_category(category_id) price = find_price(price_id) profile = find_profile(user) if profile.balance #{"<"} price.cost: raise ValueError decrease_balance(profile, price.cost) save_profile(profile) expires = calculate_period(price.period) subscription = create_subscription( profile, category, expires) notification = send_notification( 'subscription', profile, category.name) section h2 Business object problems ol li.fragment Mixed state, implementation and specification li.fragment Growth problem li.fragment Top-down architecture section img(src="https://raw.githubusercontent.com/dry-python/brand/master/logo/dry-python.png").plain p A set of libraries for pluggable business logic components. p Answers how we decompose and organize business logic. section img(src="https://raw.githubusercontent.com/dry-python/brand/master/logo/stories.png").plain p Define a user story in the business transaction DSL. p Separate state, implementation and specification. section h2 DSL pre code.python. from stories import story, arguments class Subscription: @story @arguments('category_id', 'price_id') def buy(I): I.find_category I.find_price I.find_profile I.check_balance I.persist_payment I.persist_subscription I.send_subscription_notification section h2 Context pre code.python. (Pdb) ctx Subscription.buy: find_category check_price check_purchase (PromoCode.validate) find_code (skipped) check_balance find_profile Context: category_id = 1318 # Story argument user = #{"<"}User: 3292#{">"} # Story argument category = #{"<"}Category: 1318#{">"} # Set by Subscription.find_category section(data-background-image=require("./pics/debug-toolbar.png").default data-background-size="contain") h2 DEBUG TOOLBAR br br br br br br br br br br section(data-background-image=require("./pics/pytest.png").default data-background-size="contain") h2(style="color: white") py.test section(data-background-image=require("./pics/sentry.png").default data-background-size="contain") h2 Sentry section h2 Usage ol li.fragment Story decorator build an execution plan pre code.python. class Subscription: @story def buy(I): I.find_category li.fragment Execute object methods according to plan pre code.python. def find_category(self, ctx): category = Category.objects.get( pk=ctx.category_id) return Success(category=category) li.fragment We call the story method pre code.python. subs = Subscription() subs.buy(category_id=1, price_id=1) section h2 Failures ol li.fragment Define a number of reasons pre code.python. @Subscription.buy.failures class Errors(Enum): low_balance = auto() li.fragment Failure will stop the execution of the whole story pre code.python. def check_balance(self, ctx): if ctx.profile.balance #{"<"} ctx.price.cost: return Failure(Errors.low_balance) else: return Success() li.fragment We check failure reason pre code.python. result = Subscription().buy.run(category_id=2) assert result.failed_because(Errors.low_balance) section h2 Contract ol li.fragment Define a number of variable validators pre code.python. from pydantic import BaseModel @Subscription.buy.contract class Context(BaseModel): user: User category_id: int category: Optional[Category] li.fragment Return variables from step pre code.python. def find_category(self, ctx: "Context"): category = get_category( ctx.category_id) return Success(category=category) section h2 Substories ol li.fragment Steps can be stories as well pre code.python. class Subscription: @story def buy(I): I.calculate_discount I.check_balance @story def calculate_discount(I): I.find_promo_code I.check_code_expiration li.fragment Each step can stop the execution of current substory pre code.python. def check_code_expiration(self, ctx): if ctx.promo_code.is_expired(): return Skip() else: return Success() section img(src="https://raw.githubusercontent.com/dry-python/brand/master/logo/dependencies.png").plain p Provide composition instead of inheritance. p Solves top-down approach problem. section h2 Delegate responsibility pre code.python. class Subscription: def find_category(self, ctx): category = self.load_category(ctx.category_id) return Success(category=category) def find_price(self, ctx): price = self.load_price(ctx.price_id) return Success(price=price) def __init__(self, load_category, load_price): self.load_category = load_category self.load_price = load_price section h2 Injection pre code.python. from dependencies import Injector, Package app = Package('app') class BuySubscription(Injector): buy_subscription = app.services.Subscription.buy load_category = app.repositories.load_category load_price = app.repositories.load_price load_profile = app.repositories.load_profile BuySubscription.buy_subscription(category_id=1, price_id=1) section h2 Django views pre code.python. from dependencies import operation from dependencies.contrib.django import view from django.http import HttpResponse, HttpResponseRedirect @view class BuySubscriptionView(BuySubscription): @operation def post(buy_subscription, category_id, price_id): result = buy_subscription.run(category_id, price_id) if result.is_success: return HttpResponseRedirect(to=result.value) elif result.failed_on('check_balance'): return HttpResponse('#{"<"}h1#{">"}Not enough money#{"<"}/h1#{">"}') section h2 Flask views pre code.python. from dependencies import operation from dependencies.contrib.flask import method_view from flask import redirect @method_view class BuySubscriptionView(BuySubscription): @operation def post(buy_subscription, category_id, price_id): result = buy_subscription.run(category_id, price_id) if result.is_success: return redirect(result.value) elif result.failed_on('check_balance'): return '#{"<"}h1#{">"}Not enough money#{"<"}/h1#{">"}' section h2 Celery Tasks pre code.python. from dependencies import operation from dependencies.contrib.celery import task @task class PutMoneyTask(PutMoney): @operation def run(put_money, user, amount, task): result = put_money.run(user, amount) if result.is_failure: task.on_failure(result.ctx.transaction_id) section h2 Plans ol li Delegates li Rollbacks li asyncio support li pyramid support li typing advantages li linters integration li language server section h2 Try it! pre code. $ pip install stories pre code. $ pip install dependencies section h2 Get in touch ul li a(href="https://dry-python.org/") dry-python.org li a(href="https://twitter.com/dry_py") twitter.com/dry_py li a(href="https://github.com/dry-python") github.com/dry-python li a(href="https://gitter.im/dry-python") gitter.im/dry-python