Skip to content

Commit 0dcc3fc

Browse files
committed
Merge remote-tracking branch 'atlas/master' into indexing
2 parents 5ec9b3a + 471a173 commit 0dcc3fc

78 files changed

Lines changed: 1041 additions & 1137 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

appendix_csvs.asciidoc

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
== Swapping Out the Infrastructure: pass:[<span class="keep-together">Do Everything with CSVs</span>]
44

55
This appendix is intended as a little illustration of the benefits of the
6-
Repository, Unit of Work, and Service Layer patterns.((("CSVs, doing everyting with", id="ix_CSV"))) It's intended to
6+
Repository, Unit of Work, and Service Layer patterns.((("CSVs, doing everything with", id="ix_CSV"))) It's intended to
77
follow from <<chapter_06_uow>>.
88

99
Just as we finish building out our Flask API and getting it ready for release,
1010
the business comes to us apologetically, saying they're not ready to use our API
11-
and could we build a thing that reads just batches and orders from a couple of
12-
CSVs and outputs a third with allocations.
11+
and asking if we could build a thing that reads just batches and orders from a couple of
12+
CSVs and outputs a third CSV with allocations.
1313

1414
Ordinarily this is the kind of thing that might have a team cursing and spitting
1515
and making notes for their memoirs. But not us! Oh no, we've ensured that
@@ -169,7 +169,7 @@ def test_cli_app_also_reads_existing_allocations_and_can_append_to_them(
169169

170170

171171
And we could keep hacking about and adding extra lines to that `load_batches` function,
172-
and some sort of way of tracking and saving new allocations—but we already have a model for doing that! It's called our Repository and our Unit of Work patterns.
172+
and some sort of way of tracking and saving new allocations—but we already have a model for doing that! It's called our Repository and Unit of Work patterns.
173173

174174
All we need to do ("all we need to do") is reimplement those same abstractions, but
175175
with CSVs underlying them instead of a database. And as you'll see, it really is relatively straightforward.
@@ -180,8 +180,8 @@ with CSVs underlying them instead of a database. And as you'll see, it really is
180180

181181
Here's what a CSV-based repository could look like.((("repositories", "CSV-based repository"))) It abstracts away all the
182182
logic for reading CSVs from disk, including the fact that it has to read _two
183-
different CSVs_, one for batches and one for allocations, and it just gives us
184-
the familiar `.list()` API, which gives us the illusion of an in-memory
183+
different CSVs_ (one for batches and one for allocations), and it gives us just
184+
the familiar `.list()` API, which provides the illusion of an in-memory
185185
collection of domain objects:
186186

187187
[[csv_repository]]
@@ -266,11 +266,11 @@ class CsvUnitOfWork(unit_of_work.AbstractUnitOfWork):
266266

267267

268268
And once we have that, our CLI app for reading and writing batches
269-
and allocations to CSV is pared down to what it should be: a bit
269+
and allocations to CSV is pared down to what it should bea bit
270270
of code for reading order lines, and a bit of code that invokes our
271271
_existing_ service layer:
272272

273-
273+
[role="nobreakinside less_space"]
274274
[[final_cli]]
275275
.Allocation with CSVs in nine lines (src/bin/allocate-from-csv)
276276
====
@@ -289,7 +289,7 @@ def main(folder):
289289
====
290290

291291

292-
Ta-da! _Now are y'all impressed or what_?((("CSVs, doing everyting with", startref="ix_CSV")))
292+
Ta-da! _Now are y'all impressed or what_?((("CSVs, doing everything with", startref="ix_CSV")))
293293

294294
Much love,
295295

appendix_django.asciidoc

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@
33
== Repository and Unit of Work pass:[<span class="keep-together">Patterns with Django</span>]
44

55
Suppose you wanted to use Django instead of SQLAlchemy and Flask. How
6-
might things look?((("Django", id="ix_Django")))
7-
8-
The first thing is to choose where to install it.((("Django", "installing"))) We put it in a separate
6+
might things look?((("Django", id="ix_Django"))) The first thing is to choose where to install it.((("Django", "installing"))) We put it in a separate
97
package next to our main allocation code:
108

119

@@ -49,8 +47,8 @@ package next to our main allocation code:
4947

5048
[TIP]
5149
====
52-
You can find the code for this chapter is in the
53-
https://github.com/cosmicpython/code/tree/appendix_django[appendix_django] branch on GitHub.
50+
The code for this appendix is in the
51+
appendix_django branch https://oreil.ly/A-I76[on GitHub]:
5452
5553
----
5654
git clone https://github.com/cosmicpython/code.git
@@ -62,11 +60,11 @@ git checkout appendix_django
6260

6361
=== Repository Pattern with Django
6462

65-
We used a plug in called
63+
We used a plug-in called
6664
https://github.com/pytest-dev/pytest-django[`pytest-django`] to help with test
67-
database management.((("Repository pattern", "with Django", id="ix_RepoDjango")))((("Django", "Repository pattern with", id="ix_DjangoRepo")))
65+
database management.((("pytest", "pytest-django plug-in")))((("Repository pattern", "with Django", id="ix_RepoDjango")))((("Django", "Repository pattern with", id="ix_DjangoRepo")))
6866

69-
Rewriting the first repository test was a minimal change, just rewriting
67+
Rewriting the first repository test was a minimal changejust rewriting
7068
some raw SQL with a call to the Django ORM/QuerySet language:
7169

7270

@@ -106,8 +104,8 @@ but it is still made up of familiar-looking Django code:
106104
def test_repository_can_retrieve_a_batch_with_allocations():
107105
sku = "PONY-STATUE"
108106
d_line = django_models.OrderLine.objects.create(orderid="order1", sku=sku, qty=12)
109-
d_batch1 = django_models.Batch.objects.create(reference="batch1", sku=sku, qty=100, eta=None)
110-
d_batch2 = django_models.Batch.objects.create(reference="batch2", sku=sku, qty=100, eta=None)
107+
d_b1 = django_models.Batch.objects.create(reference="batch1", sku=sku, qty=100, eta=None)
108+
d_b2 = django_models.Batch.objects.create(reference="batch2", sku=sku, qty=100, eta=None)
111109
django_models.Allocation.objects.create(line=d_line, batch=d_batch1)
112110
113111
repo = repository.DjangoRepository()
@@ -209,7 +207,7 @@ class OrderLine(models.Model):
209207

210208
<1> For value objects, `objects.get_or_create` can work, but for entities,
211209
you probably need an explicit try-get/except to handle the upsert.footnote:[
212-
`@mr-bo-jangles` suggested you might be able to use https://oreil.ly/HTq1r[`update_or_create`]
210+
`@mr-bo-jangles` suggested you might be able to use https://oreil.ly/HTq1r[`update_or_create`],
213211
but that's beyond our Django-fu.]
214212

215213
<2> We've shown the most complex example here. If you do decide to do this,
@@ -220,7 +218,7 @@ class OrderLine(models.Model):
220218

221219

222220
NOTE: As in <<chapter_02_repository>>, we use dependency inversion.
223-
The ORM (Django) depends on the model, and not the other way around.((("Repository pattern", "with Django", startref="ix_RepoDjango")))((("Django", "Repository pattern with", startref="ix_DjangoRepo")))
221+
The ORM (Django) depends on the model and not the other way around.((("Repository pattern", "with Django", startref="ix_RepoDjango")))((("Django", "Repository pattern with", startref="ix_DjangoRepo")))
224222

225223

226224

@@ -309,7 +307,7 @@ class DjangoUnitOfWork(AbstractUnitOfWork):
309307
====
310308

311309
<1> `set_autocommit(False)` was the best way to tell Django to stop
312-
automatically committing each ORM operation immediately, and
310+
automatically committing each ORM operation immediately, and to
313311
begin a transaction.
314312

315313
<2> Then we use the explicit rollback and commits.
@@ -382,7 +380,7 @@ high).
382380
Because Django is so tightly coupled to the database, you have to use helpers
383381
like `pytest-django` and think carefully about test databases, right from
384382
the very first line of code, in a way that we didn't have to when we started
385-
out with our pure domain model.
383+
out with our pure domain model.((("pytest", "pytest-django plug-in")))
386384

387385
But at a higher level, the entire reason that Django is so great
388386
is that it's designed around the sweet spot of making it easy to build CRUD
@@ -401,13 +399,12 @@ to a Django app?((("Django", "applying patterns to Django app"))) We'd say the f
401399

402400
* The Repository and Unit of Work patterns are going to be quite a lot of work. The
403401
main thing they will buy you in the short term is faster unit tests, so
404-
evaluate whether that feels worth it in your case. In the longer term, they
402+
evaluate whether that benefit feels worth it in your case. In the longer term, they
405403
decouple your app from Django and the database, so if you anticipate wanting
406404
to migrate away from either of those, Repository and UoW are a good idea.
407405

408406
* The Service Layer pattern might be of interest if you're seeing a lot of duplication in
409-
your _views.py_. It can be a good way of thinking about your use cases,
410-
separately from your web endpoints.
407+
your _views.py_. It can be a good way of thinking about your use cases separately from your web endpoints.
411408

412409
* You can still theoretically do DDD and domain modeling with Django models,
413410
tightly coupled as they are to the database; you may be slowed by
@@ -422,23 +419,21 @@ https://oreil.ly/Nbpjj[word
422419
in the Django community] is that people find that the fat models approach runs into
423420
scalability problems of its own, particularly around managing interdependencies
424421
between apps. In those cases, there's a lot to be said for extracting out a
425-
business logic or domain layer to sit between your views and forms, and
422+
business logic or domain layer to sit between your views and forms and
426423
your _models.py_, which you can then keep as minimal as possible.
427424

428425
=== Steps Along the Way
429426

430427
Suppose you're working on a Django project that you're not sure is going
431428
to get complex enough to warrant the patterns we recommend, but you still
432429
want to put a few steps in place to make your life easier, both in the medium
433-
term, and if you want to migrate to some of our patterns later.((("Django", "applying patterns to Django app", "steps along the way"))) Consider the following:
430+
term and if you want to migrate to some of our patterns later.((("Django", "applying patterns to Django app", "steps along the way"))) Consider the following:
434431

435-
* One piece of advice we've heard is to put a __logic.py__ into every Django app,
436-
from day one. This gives you a place to put business logic, and to keep your
432+
* One piece of advice we've heard is to put a __logic.py__ into every Django app from day one. This gives you a place to put business logic, and to keep your
437433
forms, views, and models free of business logic. It can become a stepping-stone
438434
for moving to a fully decoupled domain model and/or service layer later.
439435

440-
* A business-logic layer might start out working with Django model objects,
441-
and only later become fully decoupled from the framework and work on
436+
* A business-logic layer might start out working with Django model objects and only later become fully decoupled from the framework and work on
442437
plain Python data structures.
443438

444439
* For the read side, you can get some of the benefits of CQRS by putting reads
@@ -449,10 +444,10 @@ term, and if you want to migrate to some of our patterns later.((("Django", "app
449444
concerns will cut across them.
450445

451446

452-
NOTE: We'd like to give a shout out to David Seddon and Ashia Zawaduk for
453-
talking through some of the ideas in this chapter. They did their best to
447+
NOTE: We'd like to give a shout-out to David Seddon and Ashia Zawaduk for
448+
talking through some of the ideas in this appendix. They did their best to
454449
stop us from saying anything really stupid about a topic we don't really
455450
have enough personal experience of, but they may have failed.
456451

457452
For more ((("Django", startref="ix_Django")))thoughts and actual lived experience dealing with existing
458-
applications, refer to the <<epilogue_1_how_to_get_there_from_here>>.
453+
applications, refer to the <<epilogue_1_how_to_get_there_from_here, epilogue>>.

appendix_ds1_table.asciidoc

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ image::images/apwp_aa01.png["diagram showing all components: flask+eventconsumer
2020
__Defines the business logic.__
2121

2222

23-
| Entity | A domain object whose attributes may change, but that has a recognizable identity over time.
23+
| Entity | A domain object whose attributes may change but that has a recognizable identity over time.
2424

2525
| Value object | An immutable domain object whose attributes entirely define it. It is fungible with other identical objects.
2626

@@ -34,7 +34,7 @@ __Defines the business logic.__
3434

3535
__Defines the jobs the system should perform and orchestrates different components.__
3636

37-
| Handler | Receives a command or event and performs what needs to happen.
37+
| Handler | Receives a command or an event and performs what needs to happen.
3838
| Unit of work | Abstraction around data integrity. Each unit of work represents an atomic update. Makes repositories available. Tracks new events on retrieved aggregates.
3939
| Message bus (internal) | Handles commands and events by routing them to the appropriate handler.
4040

@@ -50,7 +50,7 @@ to the outside world (I/O).__
5050

5151
__Translate external inputs into calls into the service layer.__
5252

53-
| Web | Receives web requests and translates them into Commands, passing them to the Internal Message Bus.
53+
| Web | Receives web requests and translates them into commands, passing them to the internal message bus.
5454
| Event consumer | Reads events from the external message bus and translates them into commands, passing them to the internal message bus.
5555

5656
| N/A | External message bus (message broker) | A piece of infrastructure that different services use to intercommunicate, via events.((("architecture, summary diagram and table", startref="ix_archsumm")))

appendix_project_structure.asciidoc

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ be of interest to outline the moving parts.((("projects", "template project stru
88

99
[TIP]
1010
====
11-
The code for this chapter is in the
12-
https://github.com/cosmicpython/code/tree/appendix_project_structure[appendix_project_structure] branch on GitHub.
11+
The code for this appendix is in the
12+
appendix_project_structure branch https://oreil.ly/1rDRC[on GitHub]:
1313
1414
----
1515
git clone https://github.com/cosmicpython/code.git
@@ -69,7 +69,7 @@ The basic folder structure looks like this:
6969
====
7070

7171
<1> Our _docker-compose.yml_ and our _Dockerfile_ are the main bits of configuration
72-
for the containers that run our app, and can also run the tests (for CI). A
72+
for the containers that run our app, and they can also run the tests (for CI). A
7373
more complex project might have several Dockerfiles, although we've found that
7474
minimizing the number of images is usually a good idea.footnote:[Splitting
7575
out images for production and testing is sometimes a good idea, but we've tended
@@ -85,20 +85,20 @@ The basic folder structure looks like this:
8585
team knows Python (or at least knows it better than Bash!).] This is optional. You could just use
8686
`docker-compose` and `pytest` directly, but if nothing else, it's nice to
8787
have all the "common commands" in a list somewhere, and unlike
88-
documentation, a Makefile is code so it has less tendency to become out-of-date.
88+
documentation, a Makefile is code so it has less tendency to become out of date.
8989

9090
<3> All the source code for our app, including the domain model, the
9191
Flask app, and infrastructure code, lives in a Python package inside
9292
_src_,footnote:[https://hynek.me/articles/testing-packaging["Testing and Packaging"] by Hynek Schlawack provides more information on _src_ folders.]
9393
which we install using `pip install -e` and the _setup.py_ file. This makes
9494
imports easy. Currently, the structure within this module is totally flat,
9595
but for a more complex project, you'd expect to grow a folder hierarchy
96-
including _domain_model/_, _infrastructure/_, _services/_, and _api/_.
96+
that includes _domain_model/_, _infrastructure/_, _services/_, and _api/_.
9797

9898

9999
<4> Tests live in their own folder. Subfolders distinguish different test
100100
types and allow you to run them separately. We can keep shared fixtures
101-
(_conftest.py_) in the main tests folder, and nest more specific ones if we
101+
(_conftest.py_) in the main tests folder and nest more specific ones if we
102102
wish. This is also the place to keep _pytest.ini_.
103103

104104

@@ -120,7 +120,7 @@ config settings for the following:
120120

121121
- Running on the containers themselves, with "real" ports and hostnames
122122

123-
- Different container environments (dev, staging, prod, and so on).
123+
- Different container environments (dev, staging, prod, and so on)
124124

125125
Configuration through environment variables as suggested by the
126126
https://12factor.net/config[12-factor] manifesto will solve this problem,
@@ -169,17 +169,16 @@ An elegant Python package called
169169
https://github.com/hynek/environ-config[_environ-config_] is worth looking
170170
at if you get tired of hand-rolling your own environment-based config functions.
171171

172-
TIP: Don't let this config module become a dumping ground full of things that
173-
are only vaguely related to config, and is then imported all over the place.
172+
TIP: Don't let this config module become a dumping ground that is full of things only vaguely related to config and that is then imported all over the place.
174173
Keep things immutable and modify them only via environment variables.
175174
If you decide to use a <<chapter_13_dependency_injection,bootstrap script>>,
176-
you can make it the only place (other than tests) that config is imported.
175+
you can make it the only place (other than tests) that config is imported to.
177176

178177
=== Docker-Compose and Containers Config
179178

180179
We use a lightweight Docker container orchestration tool called _docker-compose_.
181180
It's main configuration is via a YAML file (sigh):footnote:[Harry is a bit YAML-weary.
182-
It's _everywhere_ and yet he can never remember the syntax or how it's supposed
181+
It's _everywhere_, and yet he can never remember the syntax or how it's supposed
183182
to indent.]
184183

185184

@@ -242,7 +241,7 @@ services:
242241
local dev machine and the container, the `PYTHONDONTWRITEBYTECODE` environment variable
243242
tells Python to not write _.pyc_ files, and that will save you from
244243
having millions of root-owned files sprinkled all over your local filesystem,
245-
being all annoying to delete, and causing weird Python compiler errors besides.
244+
being all annoying to delete and causing weird Python compiler errors besides.
246245

247246
<6> Mounting our source and test code as `volumes` means we don't need to rebuild
248247
our containers every time we make a code change.
@@ -300,8 +299,8 @@ setup(
300299
That's all you need. `packages=` specifies the names of subfolders that you
301300
want to install as top-level modules. The `name` entry is just cosmetic, but
302301
it's required. For a package that's never actually going to hit PyPI, it'll
303-
do fine.footnote:[For more _setup.py_ tips see
304-
https://hynek.me/articles/testing-packaging[this article on packaging] by Hynek.]
302+
do fine.footnote:[For more _setup.py_ tips, see
303+
https://oreil.ly/KMWDz[this article on packaging] by Hynek.]
305304

306305

307306
=== Dockerfile
@@ -344,13 +343,13 @@ CMD flask run --host=0.0.0.0 --port=80
344343
prod dependencies; we haven't here, for simplicity)
345344
<3> Copying and installing our source
346345
<4> Optionally configuring a default startup command (you'll probably override
347-
this a lot from the command-line)
346+
this a lot from the command line)
348347

349348
TIP: One thing to note is that we install things in the order of how frequently they
350349
are likely to change. This allows us to maximize Docker build cache reuse. I
351-
can't tell you how much pain and frustration underlies this lesson. For this,
350+
can't tell you how much pain and frustration underlies this lesson. For this
352351
and many more Python Dockerfile improvement tips, check out
353-
https://pythonspeed.com/docker[Production-Ready Docker Packaging].
352+
https://pythonspeed.com/docker["Production-Ready Docker Packaging"].
354353

355354
=== Tests
356355

0 commit comments

Comments
 (0)