-
-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathintroducing-dry-python.html
More file actions
153 lines (136 loc) · 12.1 KB
/
introducing-dry-python.html
File metadata and controls
153 lines (136 loc) · 12.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
<!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,maximum-scale=1,user-scalable=no"><title>Introducing dry-python</title><link href="reveal.css" rel="stylesheet"></head><body><div class="reveal"><div class="slides"><section><h2>Introducing</h2><h1><a href="https://dry-python.org/">dry-python</a></h1><h4>Artem Malyshev</h4></section><section><h2>BIO</h2><ul><li>Co-Founder at <a href="https://drylabs.io/">drylabs.io</a></li><li><a href="https://dry-python.org/">dry-python.org</a></li><li>Django Channels 1.0</li><li>5 years of experience in Python</li></ul></section><section><h2>Code is...</h2><p class="fragment"><b>hard</b></p><p class="fragment"><b>and frustrating</b></p><p class="fragment">Let's consider we're developing subscription button for a web service</p></section><section data-background-image="cdb5a8286d2f91cc4960fa5c1a9711c2.jpg" data-background-size="contain"><h2 style="color: white; background-color: black">Startup</h2><br><br><br><br><br><br><br><br></section><section><h2>Micro framework</h2><ol><li class="fragment">Long handlers</li><li class="fragment">Lots of "if" statements</li></ol></section><section><h2>Long handlers</h2><pre><code class="python"> 85 @app.route('/subscriptions/')
86 def buy_subscription(page):
...
121 if props[-1].endswith('$'):
122 -> props[-1] = props[-1][:-1]
123</code></pre><pre class="fragment"><code class="python">Traceback (most recent call last):
File "views.py", line 1027, in buy_subscription
ZeroDivisionError: division by zero</code></pre></section><section data-background-image="3631261faf85567f77566aba22830b8b.jpg" data-background-size="contain"><h2 style="color: white; background-color: black">Enterprise</h2><br><br><br><br><br><br><br><br></section><section><h2>Big framework</h2><ol><li class="fragment">You need method flowchart</li><li class="fragment">Zig-zag in the traceback</li><li class="fragment">Framework internals leak</li></ol></section><section><h2>Implicit API</h2><pre><code class="python">class SubscriptionViewSet(viewsets.ModelViewSet):
queryset = Subscription.objects.all()
serializer_class = SubscriptionSerializer
permission_classes = (CanSubscribe,)
filter_class = SubscriptionFilter</code></pre><ol><li class="fragment">What exactly does this class do?</li><li class="fragment">How to use it?</li></ol></section><section data-background-image="09172fe1c142acb120d0c205f28f435e.png" data-background-size="contain"><br></section><section><h2>Framework internals leak</h2><pre><code class="python">class SubscriptionSerializer(Serializer):
category_id = IntegerField()
price_id = IntegerField()</code></pre><pre class="fragment"><code class="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)</code></pre></section><section><h2>As a result code is...</h2><ol><li class="fragment">Fragile</li><li class="fragment">Hard to reason about</li><li class="fragment">Time-consuming</li></ol></section><section><img src="8414a0e844a1f90c2f79d3c3e89e916b.png"></section><section><blockquote><p>If your code is crap, stickies on the wall won't help.</p><a href="https://twitter.com/henrikkniberg">@HenrikKniberg</a><img src="e4f51d176293502ca62a354118e81189.jpg"></blockquote></section><section><h2>Service layer</h2><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><p><b>by Randy Stafford</b></p></section><section><h2>Business objects</h2><pre><code class="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)</code></pre></section><section><h2>Business object problems</h2><ol><li class="fragment">Mixed state, implementation and specification</li><li class="fragment">Growth problem</li><li class="fragment">Top-down architecture</li></ol></section><section><img class="plain" src="https://raw.githubusercontent.com/dry-python/brand/master/logo/dry-python.png"><p>A set of libraries for pluggable business logic components.</p><p>Answers how we decompose and organize business logic.</p></section><section><img class="plain" src="https://raw.githubusercontent.com/dry-python/brand/master/logo/stories.png"><p>Define a user story in the business transaction DSL.</p><p>Separate state, implementation and specification.</p></section><section><h2>DSL</h2><pre><code class="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</code></pre></section><section><h2>Context</h2><pre><code class="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</code></pre></section><section data-background-image="41a288079292c670fc6adbb991c81b5d.png" data-background-size="contain"><h2>DEBUG TOOLBAR</h2><br><br><br><br><br><br><br><br><br><br></section><section data-background-image="2112306ed16594557ec48c5aadcb9840.png" data-background-size="contain"><h2 style="color: white">py.test</h2></section><section data-background-image="ea36d933df7211c5a711818f2145d767.png" data-background-size="contain"><h2>Sentry</h2></section><section><h2>Usage</h2><ol><li class="fragment">Story decorator build an execution plan<pre><code class="python">class Subscription:
@story
def buy(I):
I.find_category</code></pre></li><li class="fragment">Execute object methods according to plan<pre><code class="python">def find_category(self, ctx):
category = Category.objects.get(
pk=ctx.category_id)
return Success(category=category)</code></pre></li><li class="fragment">We call the story method<pre><code class="python">subs = Subscription()
subs.buy(category_id=1, price_id=1)</code></pre></li></ol></section><section><h2>Failures</h2><ol><li class="fragment">Define a number of reasons<pre><code class="python">@Subscription.buy.failures
class Errors(Enum):
low_balance = auto()</code></pre></li><li class="fragment">Failure will stop the execution of the whole story<pre><code class="python">def check_balance(self, ctx):
if ctx.profile.balance < ctx.price.cost:
return Failure(Errors.low_balance)
else:
return Success()</code></pre></li><li class="fragment">We check failure reason<pre><code class="python">result = Subscription().buy.run(category_id=2)
assert result.failed_because(Errors.low_balance)</code></pre></li></ol></section><section><h2>Contract</h2><ol><li class="fragment">Define a number of variable validators<pre><code class="python">from pydantic import BaseModel
@Subscription.buy.contract
class Context(BaseModel):
user: User
category_id: int
category: Optional[Category]</code></pre></li><li class="fragment">Return variables from step<pre><code class="python">def find_category(self, ctx: "Context"):
category = get_category(
ctx.category_id)
return Success(category=category)</code></pre></li></ol></section><section><h2>Substories</h2><ol><li class="fragment">Steps can be stories as well<pre><code class="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</code></pre></li><li class="fragment">Each step can stop the execution of current substory<pre><code class="python">def check_code_expiration(self, ctx):
if ctx.promo_code.is_expired():
return Skip()
else:
return Success()</code></pre></li></ol></section><section><img class="plain" src="https://raw.githubusercontent.com/dry-python/brand/master/logo/dependencies.png"><p>Provide composition instead of inheritance.</p><p>Solves top-down approach problem.</p></section><section><h2>Delegate responsibility</h2><pre><code class="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</code></pre></section><section><h2>Injection</h2><pre><code class="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)</code></pre></section><section><h2>Django views</h2><pre><code class="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>')</code></pre></section><section><h2>Flask views</h2><pre><code class="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>'</code></pre></section><section><h2>Celery Tasks</h2><pre><code class="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)</code></pre></section><section><h2>Plans</h2><ol><li>Delegates</li><li>Rollbacks</li><li>asyncio support</li><li>pyramid support</li><li>typing advantages</li><li>linters integration</li><li>language server</li></ol></section><section><h2>Try it!</h2><pre><code>$ pip install stories</code></pre><pre><code>$ pip install dependencies</code></pre></section><section><h2>Get in touch</h2><ul><li><a href="https://dry-python.org/">dry-python.org</a></li><li><a href="https://twitter.com/dry_py">twitter.com/dry_py</a></li><li><a href="https://github.com/dry-python">github.com/dry-python</a></li><li><a href="https://gitter.im/dry-python">gitter.im/dry-python</a></li></ul></section></div></div><script src="reveal.js"></script></body></html>