├── .gitignore ├── .gitmodules ├── .travis.yml ├── Makefile ├── Readme.md ├── appendix_csvs.asciidoc ├── appendix_django.asciidoc ├── appendix_ds1_table.asciidoc ├── appendix_project_structure.asciidoc ├── appendix_validation.asciidoc ├── atlas.json ├── author_bio.html ├── book.asciidoc ├── callouts ├── 1.pdf ├── 1.png ├── 10.pdf ├── 10.png ├── 11.pdf ├── 11.png ├── 12.pdf ├── 12.png ├── 13.pdf ├── 13.png ├── 14.pdf ├── 14.png ├── 15.pdf ├── 15.png ├── 16.pdf ├── 16.png ├── 17.pdf ├── 17.png ├── 18.pdf ├── 18.png ├── 19.pdf ├── 19.png ├── 2.pdf ├── 2.png ├── 20.pdf ├── 20.png ├── 21.pdf ├── 21.png ├── 22.pdf ├── 22.png ├── 23.pdf ├── 23.png ├── 24.pdf ├── 24.png ├── 25.pdf ├── 25.png ├── 26.pdf ├── 26.png ├── 27.pdf ├── 27.png ├── 28.pdf ├── 28.png ├── 29.pdf ├── 29.png ├── 3.pdf ├── 3.png ├── 30.pdf ├── 30.png ├── 31.pdf ├── 31.png ├── 32.pdf ├── 32.png ├── 33.pdf ├── 33.png ├── 34.pdf ├── 34.png ├── 35.pdf ├── 35.png ├── 36.pdf ├── 36.png ├── 37.pdf ├── 37.png ├── 38.pdf ├── 38.png ├── 39.pdf ├── 39.png ├── 4.pdf ├── 4.png ├── 5.pdf ├── 5.png ├── 6.pdf ├── 6.png ├── 7.pdf ├── 7.png ├── 8.pdf ├── 8.png ├── 9.pdf └── 9.png ├── chapter_01_domain_model.asciidoc ├── chapter_02_repository.asciidoc ├── chapter_03_abstractions.asciidoc ├── chapter_04_service_layer.asciidoc ├── chapter_05_high_gear_low_gear.asciidoc ├── chapter_06_uow.asciidoc ├── chapter_07_aggregate.asciidoc ├── chapter_08_events_and_message_bus.asciidoc ├── chapter_09_all_messagebus.asciidoc ├── chapter_10_commands.asciidoc ├── chapter_11_external_events.asciidoc ├── chapter_12_cqrs.asciidoc ├── chapter_13_dependency_injection.asciidoc ├── chapters.py ├── checkout-branches-for-ci.py ├── colo.html ├── copyright.html ├── cover.html ├── epilogue_1_how_to_get_there_from_here.asciidoc ├── fix-branches.py ├── images ├── C4.puml ├── C4_Component.puml ├── C4_Container.puml ├── C4_Context.puml ├── apwp_0001.png ├── apwp_0002.png ├── apwp_0101.png ├── apwp_0102.png ├── apwp_0103.png ├── apwp_0104.png ├── apwp_0201.png ├── apwp_0202.png ├── apwp_0203.png ├── apwp_0204.png ├── apwp_0205.png ├── apwp_0206.png ├── apwp_0301.png ├── apwp_0302.png ├── apwp_0401.png ├── apwp_0402.png ├── apwp_0403.png ├── apwp_0404.png ├── apwp_0405.png ├── apwp_0501.png ├── apwp_0601.png ├── apwp_0602.png ├── apwp_0701.png ├── apwp_0702.png ├── apwp_0703.png ├── apwp_0704.png ├── apwp_0705.png ├── apwp_0801.png ├── apwp_0901.png ├── apwp_0902.png ├── apwp_0903.png ├── apwp_0904.png ├── apwp_1101.png ├── apwp_1102.png ├── apwp_1103.png ├── apwp_1104.png ├── apwp_1105.png ├── apwp_1106.png ├── apwp_1201.png ├── apwp_1202.png ├── apwp_1301.png ├── apwp_1302.png ├── apwp_1303.png ├── apwp_aa01.png ├── apwp_ep01.png ├── apwp_ep02.png ├── apwp_ep03.png ├── apwp_ep04.png ├── apwp_ep05.png ├── apwp_ep06.png ├── apwp_p101.png ├── apwp_p201.png └── cover.png ├── introduction.asciidoc ├── ix.html ├── license.txt ├── maps.drawio ├── mypy.ini ├── outline.md ├── part1.asciidoc ├── part2.asciidoc ├── plantuml.cfg ├── preface.asciidoc ├── print_figure_numbers_xref_to_image_filenames.py ├── proposal.md ├── push-branches.py ├── pytest.ini ├── rebase-appendices.sh ├── rebase-chapters.sh ├── render-diagrams.py ├── renumber-chapters.py ├── requirements.txt ├── reset-exercise-branches.py ├── tests.py ├── theme ├── asciidoctor-clean.custom.css ├── epub │ ├── epub.css │ ├── epub.xsl │ └── layout.html ├── mobi │ ├── layout.html │ ├── mobi.css │ └── mobi.xsl └── pdf │ ├── pdf.css │ └── pdf.xsl ├── titlepage.html ├── toc.html ├── tools ├── figure_renaming_report.tsv └── intakereport.txt ├── travis-deploy-key.enc ├── update-exercise-branch.py └── uppercase-titles.py /.gitignore: -------------------------------------------------------------------------------- 1 | chapter_*.html 2 | appendix_*.html 3 | preface.html 4 | introduction.html 5 | .venv 6 | .mypy_cache 7 | .env 8 | abstractions.html 9 | part2.html 10 | part1.html 11 | acknowledgements.html 12 | .asciidoctor 13 | epilogue_1_how_to_get_there_from_here.html 14 | epilogue_2_footguns.html 15 | images/*.html 16 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "code"] 2 | path = code 3 | url = git@github.com:cosmicpython/code.git 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: python 3 | python: 3.8 4 | install: 5 | - gem install asciidoctor coderay 6 | - pip install -r requirements.txt 7 | script: 8 | - make html update-code test 9 | git: 10 | submodules: false 11 | before_install: 12 | - sudo apt-get install -y tree python-pygments 13 | - sed -i s_git@github.com:_https://github.com/_ .gitmodules 14 | - git submodule update --init --recursive 15 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | html: 2 | asciidoctor \ 3 | -a stylesheet=theme/asciidoctor-clean.custom.css \ 4 | -a source-highlighter=pygments \ 5 | -a pygments-style=friendly \ 6 | -a '!example-caption' \ 7 | -a sectanchors \ 8 | *.asciidoc 9 | 10 | test: html 11 | pytest tests.py --tb=short -vv 12 | 13 | update-code: 14 | # git submodule update --init --recursive 15 | cd code && git fetch 16 | ./checkout-branches-for-ci.py 17 | 18 | count-todos: 19 | ls *.asciidoc | xargs grep -c TODO | sed s/:/\\t/ 20 | 21 | diagrams: html 22 | ./render-diagrams.py $(CHAP) 23 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Book repo 2 | 3 | | Book | Code | 4 | | ---- | ---- | 5 | | [](https://travis-ci.org/cosmicpython/book) | [](https://travis-ci.org/cosmicpython/code) | 6 | 7 | 8 | ## Table of Contents 9 | 10 | O'Reilly have generously said that we will be able to publish this book under a [CC license](license.txt), 11 | In the meantime, pull requests, typofixes, and more substantial feedback + suggestions are enthusiastically solicited. 12 | 13 | | Chapter | | 14 | | ------- | ----- | 15 | | [Preface](preface.asciidoc) | | 16 | | [Introduction: Why do our designs go wrong?](introduction.asciidoc)| || 17 | | [**Part 1 Intro**](part1.asciidoc) | | 18 | | [Chapter 1: Domain Model](chapter_01_domain_model.asciidoc) | [](https://travis-ci.org/cosmicpython/code) | 19 | | [Chapter 2: Repository](chapter_02_repository.asciidoc) | [](https://travis-ci.org/cosmicpython/code) | 20 | | [Chapter 3: Interlude: Abstractions](chapter_03_abstractions.asciidoc) | | 21 | | [Chapter 4: Service Layer (and Flask API)](chapter_04_service_layer.asciidoc) | [](https://travis-ci.org/cosmicpython/code) | 22 | | [Chapter 5: TDD in High Gear and Low Gear](chapter_05_high_gear_low_gear.asciidoc) | [](https://travis-ci.org/cosmicpython/code) | 23 | | [Chapter 6: Unit of Work](chapter_06_uow.asciidoc) | [](https://travis-ci.org/cosmicpython/code) | 24 | | [Chapter 7: Aggregates](chapter_07_aggregate.asciidoc) | [](https://travis-ci.org/cosmicpython/code) | 25 | | [**Part 2 Intro**](part2.asciidoc) | | 26 | | [Chapter 8: Domain Events and a Simple Message Bus](chapter_08_events_and_message_bus.asciidoc) | [](https://travis-ci.org/cosmicpython/code) | 27 | | [Chapter 9: Going to Town on the MessageBus](chapter_09_all_messagebus.asciidoc) | [](https://travis-ci.org/cosmicpython/code) | 28 | | [Chapter 10: Commands](chapter_10_commands.asciidoc) | [](https://travis-ci.org/cosmicpython/code) | 29 | | [Chapter 11: External Events for Integration](chapter_11_external_events.asciidoc) | [](https://travis-ci.org/cosmicpython/code) | 30 | | [Chapter 12: CQRS](chapter_12_cqrs.asciidoc) | [](https://travis-ci.org/cosmicpython/code) | 31 | | [Chapter 13: Dependency Injection](chapter_13_dependency_injection.asciidoc) | [](https://travis-ci.org/cosmicpython/code) | 32 | | [Epilogue: How do I get there from here?](epilogue_1_how_to_get_there_from_here.asciidoc) | | 33 | | [Appendix A: Recap table](appendix_ds1_table.asciidoc) | | 34 | | [Appendix B: Project Structure](appendix_project_structure.asciidoc) | [](https://travis-ci.org/cosmicpython/code) | 35 | | [Appendix C: A major infrastructure change, made easy](appendix_csvs.asciidoc) | [](https://travis-ci.org/cosmicpython/code) | 36 | | [Appendix D: Django](appendix_django.asciidoc) | [](https://travis-ci.org/cosmicpython/code) | 37 | | [Appendix F: Validation](appendix_validation.asciidoc) | | 38 | 39 | 40 | 41 | 42 | Below is just instructions for me and bob really. 43 | 44 | ## Dependencies: 45 | 46 | * asciidoctor 47 | * Pygments (for syntax higlighting) 48 | * asciidoctor-diagram (to render images from the text sources in [`./images`](./images)) 49 | 50 | ```sh 51 | gem install asciidoctor 52 | python2 -m pip install --user pygments 53 | gem install pygments.rb 54 | gem install asciidoctor-diagram 55 | ``` 56 | 57 | 58 | ## Commands 59 | 60 | ```sh 61 | make html # builds local .html versions of each chapter 62 | make test # does a sanity-check of the code listings 63 | ``` 64 | 65 | -------------------------------------------------------------------------------- /appendix_csvs.asciidoc: -------------------------------------------------------------------------------- 1 | [[appendix_csvs]] 2 | [appendix] 3 | == Swapping Out the Infrastructure: [.keep-together]#Do Everything with CSVs# 4 | 5 | ((("CSVs, doing everything with", id="ix_CSV"))) 6 | This appendix is intended as a little illustration of the benefits of the 7 | Repository, Unit of Work, and Service Layer patterns. It's intended to 8 | follow from <<chapter_06_uow>>. 9 | 10 | Just as we finish building out our Flask API and getting it ready for release, 11 | the business comes to us apologetically, saying they're not ready to use our API 12 | and asking if we could build a thing that reads just batches and orders from a couple of 13 | CSVs and outputs a third CSV with allocations. 14 | 15 | Ordinarily this is the kind of thing that might have a team cursing and spitting 16 | and making notes for their memoirs. But not us! Oh no, we've ensured that 17 | our infrastructure concerns are nicely decoupled from our domain model and 18 | service layer. Switching to CSVs will be a simple matter of writing a couple 19 | of new `Repository` and `UnitOfWork` classes, and then we'll be able to reuse 20 | _all_ of our logic from the domain layer and the service layer. 21 | 22 | Here's an E2E test to show you how the CSVs flow in and out: 23 | 24 | [[first_csv_test]] 25 | .A first CSV test (tests/e2e/test_csv.py) 26 | ==== 27 | [source,python] 28 | ---- 29 | def test_cli_app_reads_csvs_with_batches_and_orders_and_outputs_allocations(make_csv): 30 | sku1, sku2 = random_ref("s1"), random_ref("s2") 31 | batch1, batch2, batch3 = random_ref("b1"), random_ref("b2"), random_ref("b3") 32 | order_ref = random_ref("o") 33 | make_csv("batches.csv", [ 34 | ["ref", "sku", "qty", "eta"], 35 | [batch1, sku1, 100, ""], 36 | [batch2, sku2, 100, "2011-01-01"], 37 | [batch3, sku2, 100, "2011-01-02"], 38 | ]) 39 | orders_csv = make_csv("orders.csv", [ 40 | ["orderid", "sku", "qty"], 41 | [order_ref, sku1, 3], 42 | [order_ref, sku2, 12], 43 | ]) 44 | 45 | run_cli_script(orders_csv.parent) 46 | 47 | expected_output_csv = orders_csv.parent / "allocations.csv" 48 | with open(expected_output_csv) as f: 49 | rows = list(csv.reader(f)) 50 | assert rows == [ 51 | ["orderid", "sku", "qty", "batchref"], 52 | [order_ref, sku1, "3", batch1], 53 | [order_ref, sku2, "12", batch2], 54 | ] 55 | ---- 56 | ==== 57 | 58 | Diving in and implementing without thinking about repositories and all 59 | that jazz, you might start with something like this: 60 | 61 | 62 | [[first_cut_csvs]] 63 | .A first cut of our CSV reader/writer (src/bin/allocate-from-csv) 64 | ==== 65 | [source,python] 66 | [role="non-head"] 67 | ---- 68 | #!/usr/bin/env python 69 | import csv 70 | import sys 71 | from datetime import datetime 72 | from pathlib import Path 73 | 74 | from allocation.domain import model 75 | 76 | 77 | def load_batches(batches_path): 78 | batches = [] 79 | with batches_path.open() as inf: 80 | reader = csv.DictReader(inf) 81 | for row in reader: 82 | if row["eta"]: 83 | eta = datetime.strptime(row["eta"], "%Y-%m-%d").date() 84 | else: 85 | eta = None 86 | batches.append( 87 | model.Batch( 88 | ref=row["ref"], sku=row["sku"], qty=int(row["qty"]), eta=eta 89 | ) 90 | ) 91 | return batches 92 | 93 | 94 | def main(folder): 95 | batches_path = Path(folder) / "batches.csv" 96 | orders_path = Path(folder) / "orders.csv" 97 | allocations_path = Path(folder) / "allocations.csv" 98 | 99 | batches = load_batches(batches_path) 100 | 101 | with orders_path.open() as inf, allocations_path.open("w") as outf: 102 | reader = csv.DictReader(inf) 103 | writer = csv.writer(outf) 104 | writer.writerow(["orderid", "sku", "batchref"]) 105 | for row in reader: 106 | orderid, sku = row["orderid"], row["sku"] 107 | qty = int(row["qty"]) 108 | line = model.OrderLine(orderid, sku, qty) 109 | batchref = model.allocate(line, batches) 110 | writer.writerow([line.orderid, line.sku, batchref]) 111 | 112 | 113 | if __name__ == "__main__": 114 | main(sys.argv[1]) 115 | ---- 116 | ==== 117 | 118 | //TODO: too much vertical whitespace in this listing 119 | 120 | It's not looking too bad! And we're reusing our domain model objects 121 | and our domain service. 122 | 123 | But it's not going to work. Existing allocations need to also be part 124 | of our permanent CSV storage. We can write a second test to force us to improve 125 | things: 126 | 127 | [[second_csv_test]] 128 | .And another one, with existing allocations (tests/e2e/test_csv.py) 129 | ==== 130 | [source,python] 131 | ---- 132 | def test_cli_app_also_reads_existing_allocations_and_can_append_to_them(make_csv): 133 | sku = random_ref("s") 134 | batch1, batch2 = random_ref("b1"), random_ref("b2") 135 | old_order, new_order = random_ref("o1"), random_ref("o2") 136 | make_csv("batches.csv", [ 137 | ["ref", "sku", "qty", "eta"], 138 | [batch1, sku, 10, "2011-01-01"], 139 | [batch2, sku, 10, "2011-01-02"], 140 | ]) 141 | make_csv("allocations.csv", [ 142 | ["orderid", "sku", "qty", "batchref"], 143 | [old_order, sku, 10, batch1], 144 | ]) 145 | orders_csv = make_csv("orders.csv", [ 146 | ["orderid", "sku", "qty"], 147 | [new_order, sku, 7], 148 | ]) 149 | 150 | run_cli_script(orders_csv.parent) 151 | 152 | expected_output_csv = orders_csv.parent / "allocations.csv" 153 | with open(expected_output_csv) as f: 154 | rows = list(csv.reader(f)) 155 | assert rows == [ 156 | ["orderid", "sku", "qty", "batchref"], 157 | [old_order, sku, "10", batch1], 158 | [new_order, sku, "7", batch2], 159 | ] 160 | ---- 161 | ==== 162 | 163 | 164 | And we could keep hacking about and adding extra lines to that `load_batches` function, 165 | 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. 166 | 167 | All we need to do ("all we need to do") is reimplement those same abstractions, but 168 | with CSVs underlying them instead of a database. And as you'll see, it really is relatively straightforward. 169 | 170 | 171 | === Implementing a Repository and Unit of Work for CSVs 172 | 173 | 174 | ((("repositories", "CSV-based repository"))) 175 | Here's what a CSV-based repository could look like. It abstracts away all the 176 | logic for reading CSVs from disk, including the fact that it has to read _two 177 | different CSVs_ (one for batches and one for allocations), and it gives us just 178 | the familiar `.list()` API, which provides the illusion of an in-memory 179 | collection of domain objects: 180 | 181 | [[csv_repository]] 182 | .A repository that uses CSV as its storage mechanism (src/allocation/service_layer/csv_uow.py) 183 | ==== 184 | [source,python] 185 | ---- 186 | class CsvRepository(repository.AbstractRepository): 187 | def __init__(self, folder): 188 | self._batches_path = Path(folder) / "batches.csv" 189 | self._allocations_path = Path(folder) / "allocations.csv" 190 | self._batches = {} # type: Dict[str, model.Batch] 191 | self._load() 192 | 193 | def get(self, reference): 194 | return self._batches.get(reference) 195 | 196 | def add(self, batch): 197 | self._batches[batch.reference] = batch 198 | 199 | def _load(self): 200 | with self._batches_path.open() as f: 201 | reader = csv.DictReader(f) 202 | for row in reader: 203 | ref, sku = row["ref"], row["sku"] 204 | qty = int(row["qty"]) 205 | if row["eta"]: 206 | eta = datetime.strptime(row["eta"], "%Y-%m-%d").date() 207 | else: 208 | eta = None 209 | self._batches[ref] = model.Batch(ref=ref, sku=sku, qty=qty, eta=eta) 210 | if self._allocations_path.exists() is False: 211 | return 212 | with self._allocations_path.open() as f: 213 | reader = csv.DictReader(f) 214 | for row in reader: 215 | batchref, orderid, sku = row["batchref"], row["orderid"], row["sku"] 216 | qty = int(row["qty"]) 217 | line = model.OrderLine(orderid, sku, qty) 218 | batch = self._batches[batchref] 219 | batch._allocations.add(line) 220 | 221 | def list(self): 222 | return list(self._batches.values()) 223 | ---- 224 | ==== 225 | 226 | // TODO (hynek) re self._load(): DUDE! no i/o in init! 227 | 228 | 229 | ((("Unit of Work pattern", "UoW for CSVs"))) 230 | And here's what a UoW for CSVs would look like: 231 | 232 | 233 | 234 | [[csvs_uow]] 235 | .A UoW for CSVs: commit = csv.writer (src/allocation/service_layer/csv_uow.py) 236 | ==== 237 | [source,python] 238 | ---- 239 | class CsvUnitOfWork(unit_of_work.AbstractUnitOfWork): 240 | def __init__(self, folder): 241 | self.batches = CsvRepository(folder) 242 | 243 | def commit(self): 244 | with self.batches._allocations_path.open("w") as f: 245 | writer = csv.writer(f) 246 | writer.writerow(["orderid", "sku", "qty", "batchref"]) 247 | for batch in self.batches.list(): 248 | for line in batch._allocations: 249 | writer.writerow( 250 | [line.orderid, line.sku, line.qty, batch.reference] 251 | ) 252 | 253 | def rollback(self): 254 | pass 255 | ---- 256 | ==== 257 | 258 | 259 | And once we have that, our CLI app for reading and writing batches 260 | and allocations to CSV is pared down to what it should be—a bit 261 | of code for reading order lines, and a bit of code that invokes our 262 | _existing_ service layer: 263 | 264 | [role="nobreakinside less_space"] 265 | [[final_cli]] 266 | .Allocation with CSVs in nine lines (src/bin/allocate-from-csv) 267 | ==== 268 | [source,python] 269 | ---- 270 | def main(folder): 271 | orders_path = Path(folder) / "orders.csv" 272 | uow = csv_uow.CsvUnitOfWork(folder) 273 | with orders_path.open() as f: 274 | reader = csv.DictReader(f) 275 | for row in reader: 276 | orderid, sku = row["orderid"], row["sku"] 277 | qty = int(row["qty"]) 278 | services.allocate(orderid, sku, qty, uow) 279 | ---- 280 | ==== 281 | 282 | 283 | ((("CSVs, doing everything with", startref="ix_CSV"))) 284 | Ta-da! _Now are y'all impressed or what_? 285 | 286 | Much love, 287 | 288 | Bob and Harry 289 | -------------------------------------------------------------------------------- /appendix_django.asciidoc: -------------------------------------------------------------------------------- 1 | [[appendix_django]] 2 | [appendix] 3 | == Repository and Unit of Work [.keep-together]#Patterns with Django# 4 | 5 | ((("Django", "installing"))) 6 | ((("Django", id="ix_Django"))) 7 | Suppose you wanted to use Django instead of SQLAlchemy and Flask. How 8 | might things look? The first thing is to choose where to install it. We put it in a separate 9 | package next to our main allocation code: 10 | 11 | 12 | [[django_tree]] 13 | ==== 14 | [source,text] 15 | [role="tree"] 16 | ---- 17 | ├── src 18 | │ ├── allocation 19 | │ │ ├── __init__.py 20 | │ │ ├── adapters 21 | │ │ │ ├── __init__.py 22 | ... 23 | │ ├── djangoproject 24 | │ │ ├── alloc 25 | │ │ │ ├── __init__.py 26 | │ │ │ ├── apps.py 27 | │ │ │ ├── migrations 28 | │ │ │ │ ├── 0001_initial.py 29 | │ │ │ │ └── __init__.py 30 | │ │ │ ├── models.py 31 | │ │ │ └── views.py 32 | │ │ ├── django_project 33 | │ │ │ ├── __init__.py 34 | │ │ │ ├── settings.py 35 | │ │ │ ├── urls.py 36 | │ │ │ └── wsgi.py 37 | │ │ └── manage.py 38 | │ └── setup.py 39 | └── tests 40 | ├── conftest.py 41 | ├── e2e 42 | │ └── test_api.py 43 | ├── integration 44 | │ ├── test_repository.py 45 | ... 46 | ---- 47 | ==== 48 | 49 | 50 | [TIP] 51 | ==== 52 | The code for this appendix is in the 53 | appendix_django branch https://oreil.ly/A-I76[on GitHub]: 54 | 55 | ---- 56 | git clone https://github.com/cosmicpython/code.git 57 | cd code 58 | git checkout appendix_django 59 | ---- 60 | 61 | Code examples follows on from the end of <<chapter_06_uow>>. 62 | 63 | ==== 64 | 65 | 66 | === Repository Pattern with Django 67 | 68 | ((("pytest", "pytest-django plug-in"))) 69 | ((("Repository pattern", "with Django", id="ix_RepoDjango"))) 70 | ((("Django", "Repository pattern with", id="ix_DjangoRepo"))) 71 | We used a plugin called 72 | https://github.com/pytest-dev/pytest-django[`pytest-django`] to help with test 73 | database management. 74 | 75 | Rewriting the first repository test was a minimal change—just rewriting 76 | some raw SQL with a call to the Django ORM/QuerySet language: 77 | 78 | 79 | [[django_repo_test1]] 80 | .First repository test adapted (tests/integration/test_repository.py) 81 | ==== 82 | [source,python] 83 | ---- 84 | from djangoproject.alloc import models as django_models 85 | 86 | 87 | @pytest.mark.django_db 88 | def test_repository_can_save_a_batch(): 89 | batch = model.Batch("batch1", "RUSTY-SOAPDISH", 100, eta=date(2011, 12, 25)) 90 | 91 | repo = repository.DjangoRepository() 92 | repo.add(batch) 93 | 94 | [saved_batch] = django_models.Batch.objects.all() 95 | assert saved_batch.reference == batch.reference 96 | assert saved_batch.sku == batch.sku 97 | assert saved_batch.qty == batch._purchased_quantity 98 | assert saved_batch.eta == batch.eta 99 | ---- 100 | ==== 101 | 102 | 103 | The second test is a bit more involved since it has allocations, 104 | but it is still made up of familiar-looking Django code: 105 | 106 | [[django_repo_test2]] 107 | .Second repository test is more involved (tests/integration/test_repository.py) 108 | ==== 109 | [source,python] 110 | ---- 111 | @pytest.mark.django_db 112 | def test_repository_can_retrieve_a_batch_with_allocations(): 113 | sku = "PONY-STATUE" 114 | d_line = django_models.OrderLine.objects.create(orderid="order1", sku=sku, qty=12) 115 | d_batch1 = django_models.Batch.objects.create( 116 | reference="batch1", sku=sku, qty=100, eta=None 117 | ) 118 | d_batch2 = django_models.Batch.objects.create( 119 | reference="batch2", sku=sku, qty=100, eta=None 120 | ) 121 | django_models.Allocation.objects.create(line=d_line, batch=d_batch1) 122 | 123 | repo = repository.DjangoRepository() 124 | retrieved = repo.get("batch1") 125 | 126 | expected = model.Batch("batch1", sku, 100, eta=None) 127 | assert retrieved == expected # Batch.__eq__ only compares reference 128 | assert retrieved.sku == expected.sku 129 | assert retrieved._purchased_quantity == expected._purchased_quantity 130 | assert retrieved._allocations == { 131 | model.OrderLine("order1", sku, 12), 132 | } 133 | ---- 134 | ==== 135 | 136 | Here's how the actual repository ends up looking: 137 | 138 | 139 | [[django_repository]] 140 | .A Django repository (src/allocation/adapters/repository.py) 141 | ==== 142 | [source,python] 143 | ---- 144 | class DjangoRepository(AbstractRepository): 145 | def add(self, batch): 146 | super().add(batch) 147 | self.update(batch) 148 | 149 | def update(self, batch): 150 | django_models.Batch.update_from_domain(batch) 151 | 152 | def _get(self, reference): 153 | return ( 154 | django_models.Batch.objects.filter(reference=reference) 155 | .first() 156 | .to_domain() 157 | ) 158 | 159 | def list(self): 160 | return [b.to_domain() for b in django_models.Batch.objects.all()] 161 | ---- 162 | ==== 163 | 164 | 165 | You can see that the implementation relies on the Django models having 166 | some custom methods for translating to and from our domain model.footnote:[ 167 | The DRY-Python project people have built a tool called 168 | https://mappers.readthedocs.io/en/latest[mappers] that looks like it might 169 | help minimize boilerplate for this sort of thing.] 170 | 171 | 172 | ==== Custom Methods on Django ORM Classes to Translate to/from Our Domain Model 173 | 174 | ((("domain model", "Django custom ORM methods for conversion"))) 175 | ((("object-relational mappers (ORMs)", "Django, custom methods to translate to/from domain model"))) 176 | Those custom methods look something like this: 177 | 178 | [[django_models]] 179 | .Django ORM with custom methods for domain model conversion (src/djangoproject/alloc/models.py) 180 | ==== 181 | [source,python] 182 | ---- 183 | from django.db import models 184 | from allocation.domain import model as domain_model 185 | 186 | 187 | class Batch(models.Model): 188 | reference = models.CharField(max_length=255) 189 | sku = models.CharField(max_length=255) 190 | qty = models.IntegerField() 191 | eta = models.DateField(blank=True, null=True) 192 | 193 | @staticmethod 194 | def update_from_domain(batch: domain_model.Batch): 195 | try: 196 | b = Batch.objects.get(reference=batch.reference) #<1> 197 | except Batch.DoesNotExist: 198 | b = Batch(reference=batch.reference) #<1> 199 | b.sku = batch.sku 200 | b.qty = batch._purchased_quantity 201 | b.eta = batch.eta #<2> 202 | b.save() 203 | b.allocation_set.set( 204 | Allocation.from_domain(l, b) #<3> 205 | for l in batch._allocations 206 | ) 207 | 208 | def to_domain(self) -> domain_model.Batch: 209 | b = domain_model.Batch( 210 | ref=self.reference, sku=self.sku, qty=self.qty, eta=self.eta 211 | ) 212 | b._allocations = set( 213 | a.line.to_domain() 214 | for a in self.allocation_set.all() 215 | ) 216 | return b 217 | 218 | 219 | class OrderLine(models.Model): 220 | #... 221 | ---- 222 | ==== 223 | 224 | <1> For value objects, `objects.get_or_create` can work, but for entities, 225 | you probably need an explicit try-get/except to handle the upsert.footnote:[ 226 | `@mr-bo-jangles` suggested you might be able to use https://oreil.ly/HTq1r[`update_or_create`], 227 | but that's beyond our Django-fu.] 228 | 229 | <2> We've shown the most complex example here. If you do decide to do this, 230 | be aware that there will be boilerplate! Thankfully it's not very 231 | complex boilerplate. 232 | 233 | <3> Relationships also need some careful, custom handling. 234 | 235 | 236 | NOTE: As in <<chapter_02_repository>>, we use dependency inversion. 237 | The ORM (Django) depends on the model and not the other way around. 238 | ((("Django", "Repository pattern with", startref="ix_DjangoRepo"))) 239 | ((("Repository pattern", "with Django", startref="ix_RepoDjango"))) 240 | 241 | 242 | 243 | === Unit of Work Pattern with Django 244 | 245 | 246 | ((("Django", "Unit of Work pattern with", id="ix_DjangoUoW"))) 247 | ((("Unit of Work pattern", "with Django", id="ix_UoWDjango"))) 248 | The tests don't change too much: 249 | 250 | [[test_uow_django]] 251 | .Adapted UoW tests (tests/integration/test_uow.py) 252 | ==== 253 | [source,python] 254 | ---- 255 | def insert_batch(ref, sku, qty, eta): #<1> 256 | django_models.Batch.objects.create(reference=ref, sku=sku, qty=qty, eta=eta) 257 | 258 | 259 | def get_allocated_batch_ref(orderid, sku): #<1> 260 | return django_models.Allocation.objects.get( 261 | line__orderid=orderid, line__sku=sku 262 | ).batch.reference 263 | 264 | 265 | @pytest.mark.django_db(transaction=True) 266 | def test_uow_can_retrieve_a_batch_and_allocate_to_it(): 267 | insert_batch("batch1", "HIPSTER-WORKBENCH", 100, None) 268 | 269 | uow = unit_of_work.DjangoUnitOfWork() 270 | with uow: 271 | batch = uow.batches.get(reference="batch1") 272 | line = model.OrderLine("o1", "HIPSTER-WORKBENCH", 10) 273 | batch.allocate(line) 274 | uow.commit() 275 | 276 | batchref = get_allocated_batch_ref("o1", "HIPSTER-WORKBENCH") 277 | assert batchref == "batch1" 278 | 279 | 280 | @pytest.mark.django_db(transaction=True) #<2> 281 | def test_rolls_back_uncommitted_work_by_default(): 282 | ... 283 | 284 | @pytest.mark.django_db(transaction=True) #<2> 285 | def test_rolls_back_on_error(): 286 | ... 287 | ---- 288 | ==== 289 | 290 | <1> Because we had little helper functions in these tests, the actual 291 | main bodies of the tests are pretty much the same as they were with 292 | SQLAlchemy. 293 | 294 | <2> The `pytest-django` `mark.django_db(transaction=True)` is required to 295 | test our custom transaction/rollback behaviors. 296 | 297 | 298 | 299 | And the implementation is quite simple, although it took me a few 300 | tries to find which invocation of Django's transaction magic 301 | would work: 302 | 303 | 304 | [[start_uow_django]] 305 | .UoW adapted for Django (src/allocation/service_layer/unit_of_work.py) 306 | ==== 307 | [source,python] 308 | ---- 309 | class DjangoUnitOfWork(AbstractUnitOfWork): 310 | def __enter__(self): 311 | self.batches = repository.DjangoRepository() 312 | transaction.set_autocommit(False) #<1> 313 | return super().__enter__() 314 | 315 | def __exit__(self, *args): 316 | super().__exit__(*args) 317 | transaction.set_autocommit(True) 318 | 319 | def commit(self): 320 | for batch in self.batches.seen: #<3> 321 | self.batches.update(batch) #<3> 322 | transaction.commit() #<2> 323 | 324 | def rollback(self): 325 | transaction.rollback() #<2> 326 | ---- 327 | ==== 328 | 329 | <1> `set_autocommit(False)` was the best way to tell Django to stop 330 | automatically committing each ORM operation immediately, and to 331 | begin a transaction. 332 | 333 | <2> Then we use the explicit rollback and commits. 334 | 335 | <3> One difficulty: because, unlike with SQLAlchemy, we're not 336 | instrumenting the domain model instances themselves, the 337 | `commit()` command needs to explicitly go through all the 338 | objects that have been touched by every repository and manually 339 | update them back to the ORM. 340 | ((("Django", "Unit of Work pattern with", startref="ix_DjangoUoW"))) 341 | ((("Unit of Work pattern", "with Django", startref="ix_UoWDjango"))) 342 | 343 | 344 | 345 | === API: Django Views Are Adapters 346 | 347 | ((("adapters", "Django views"))) 348 | ((("views", "Django views as adapters"))) 349 | ((("APIs", "Django views as adapters"))) 350 | ((("Django", "views are adapters"))) 351 | The Django _views.py_ file ends up being almost identical to the 352 | old _flask_app.py_, because our architecture means it's a very 353 | thin wrapper around our service layer (which didn't change at all, by the way): 354 | 355 | 356 | [[django_views]] 357 | .Flask app -> Django views (src/djangoproject/alloc/views.py) 358 | ==== 359 | [source,python] 360 | ---- 361 | os.environ["DJANGO_SETTINGS_MODULE"] = "djangoproject.django_project.settings" 362 | django.setup() 363 | 364 | 365 | @csrf_exempt 366 | def add_batch(request): 367 | data = json.loads(request.body) 368 | eta = data["eta"] 369 | if eta is not None: 370 | eta = datetime.fromisoformat(eta).date() 371 | services.add_batch( 372 | data["ref"], data["sku"], data["qty"], eta, 373 | unit_of_work.DjangoUnitOfWork(), 374 | ) 375 | return HttpResponse("OK", status=201) 376 | 377 | 378 | @csrf_exempt 379 | def allocate(request): 380 | data = json.loads(request.body) 381 | try: 382 | batchref = services.allocate( 383 | data["orderid"], 384 | data["sku"], 385 | data["qty"], 386 | unit_of_work.DjangoUnitOfWork(), 387 | ) 388 | except (model.OutOfStock, services.InvalidSku) as e: 389 | return JsonResponse({"message": str(e)}, status=400) 390 | 391 | return JsonResponse({"batchref": batchref}, status=201) 392 | ---- 393 | ==== 394 | 395 | 396 | === Why Was This All So Hard? 397 | 398 | ((("Django", "using, difficulty of"))) 399 | OK, it works, but it does feel like more effort than Flask/SQLAlchemy. Why is 400 | that? 401 | 402 | The main reason at a low level is because Django's ORM doesn't work in the same 403 | way. We don't have an equivalent of the SQLAlchemy classical mapper, so our 404 | `ActiveRecord` and our domain model can't be the same object. Instead we have to 405 | build a manual translation layer behind the repository. That's more 406 | work (although once it's done, the ongoing maintenance burden shouldn't be too 407 | high). 408 | 409 | ((("pytest", "pytest-django plugin"))) 410 | Because Django is so tightly coupled to the database, you have to use helpers 411 | like `pytest-django` and think carefully about test databases, right from 412 | the very first line of code, in a way that we didn't have to when we started 413 | out with our pure domain model. 414 | 415 | But at a higher level, the entire reason that Django is so great 416 | is that it's designed around the sweet spot of making it easy to build CRUD 417 | apps with minimal boilerplate. But the entire thrust of our book is about 418 | what to do when your app is no longer a simple CRUD app. 419 | 420 | At that point, Django starts hindering more than it helps. Things like the 421 | Django admin, which are so awesome when you start out, become actively dangerous 422 | if the whole point of your app is to build a complex set of rules and modeling 423 | around the workflow of state changes. The Django admin bypasses all of that. 424 | 425 | === What to Do If You Already Have Django 426 | 427 | ((("Django", "applying patterns to Django app"))) 428 | So what should you do if you want to apply some of the patterns in this book 429 | to a Django app? We'd say the following: 430 | 431 | * The Repository and Unit of Work patterns are going to be quite a lot of work. The 432 | main thing they will buy you in the short term is faster unit tests, so 433 | evaluate whether that benefit feels worth it in your case. In the longer term, they 434 | decouple your app from Django and the database, so if you anticipate wanting 435 | to migrate away from either of those, Repository and UoW are a good idea. 436 | 437 | * The Service Layer pattern might be of interest if you're seeing a lot of duplication in 438 | your _views.py_. It can be a good way of thinking about your use cases separately from your web endpoints. 439 | 440 | * You can still theoretically do DDD and domain modeling with Django models, 441 | tightly coupled as they are to the database; you may be slowed by 442 | migrations, but it shouldn't be fatal. So as long as your app is not too 443 | complex and your tests not too slow, you may be able to get something out of 444 | the _fat models_ approach: push as much logic down to your models as possible, 445 | and apply patterns like Entity, Value Object, and Aggregate. However, see 446 | the following caveat. 447 | 448 | With that said, 449 | https://oreil.ly/Nbpjj[word 450 | in the Django community] is that people find that the fat models approach runs into 451 | scalability problems of its own, particularly around managing interdependencies 452 | between apps. In those cases, there's a lot to be said for extracting out a 453 | business logic or domain layer to sit between your views and forms and 454 | your _models.py_, which you can then keep as minimal as possible. 455 | 456 | === Steps Along the Way 457 | 458 | ((("Django", "applying patterns to Django app", "steps along the way"))) 459 | Suppose you're working on a Django project that you're not sure is going 460 | to get complex enough to warrant the patterns we recommend, but you still 461 | want to put a few steps in place to make your life easier, both in the medium 462 | term and if you want to migrate to some of our patterns later. Consider the following: 463 | 464 | * 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 465 | forms, views, and models free of business logic. It can become a stepping-stone 466 | for moving to a fully decoupled domain model and/or service layer later. 467 | 468 | * A business-logic layer might start out working with Django model objects and only later become fully decoupled from the framework and work on 469 | plain Python data structures. 470 | 471 | [role="pagebreak-before"] 472 | * For the read side, you can get some of the benefits of CQRS by putting reads 473 | into one place, avoiding ORM calls sprinkled all over the place. 474 | 475 | * When separating out modules for reads and modules for domain logic, it 476 | may be worth decoupling yourself from the Django apps hierarchy. Business 477 | concerns will cut across them. 478 | 479 | 480 | NOTE: We'd like to give a shout-out to David Seddon and Ashia Zawaduk for 481 | talking through some of the ideas in this appendix. They did their best to 482 | stop us from saying anything really stupid about a topic we don't really 483 | have enough personal experience of, but they may have failed. 484 | 485 | ((("Django", startref="ix_Django"))) 486 | For more thoughts and actual lived experience dealing with existing 487 | applications, refer to the <<epilogue_1_how_to_get_there_from_here, epilogue>>. 488 | -------------------------------------------------------------------------------- /appendix_ds1_table.asciidoc: -------------------------------------------------------------------------------- 1 | [[appendix_ds1_table]] 2 | [appendix] 3 | == Summary Diagram and Table 4 | 5 | ((("architecture, summary diagram and table", id="ix_archsumm"))) 6 | Here's what our architecture looks like by the end of the book: 7 | 8 | [[recap_diagram]] 9 | image::images/apwp_aa01.png["diagram showing all components: flask+eventconsumer, service layer, adapters, domain etc"] 10 | 11 | <<ds1_table>> recaps each pattern and what it does. 12 | 13 | [[ds1_table]] 14 | .The components of our architecture and what they all do 15 | [cols="1,1,2"] 16 | |=== 17 | | Layer | Component | Description 18 | 19 | .5+a| *Domain* 20 | 21 | __Defines the business logic.__ 22 | 23 | 24 | | Entity | A domain object whose attributes may change but that has a recognizable identity over time. 25 | 26 | | Value object | An immutable domain object whose attributes entirely define it. It is fungible with other identical objects. 27 | 28 | | Aggregate | Cluster of associated objects that we treat as a unit for the purpose of data changes. Defines and enforces a consistency boundary. 29 | 30 | | Event | Represents something that happened. 31 | 32 | | Command | Represents a job the system should perform. 33 | 34 | .3+a| *Service Layer* 35 | 36 | __Defines the jobs the system should perform and orchestrates different components.__ 37 | 38 | | Handler | Receives a command or an event and performs what needs to happen. 39 | | Unit of work | Abstraction around data integrity. Each unit of work represents an atomic update. Makes repositories available. Tracks new events on retrieved aggregates. 40 | | Message bus (internal) | Handles commands and events by routing them to the appropriate handler. 41 | 42 | .2+a| *Adapters* (Secondary) 43 | 44 | __Concrete implementations of an interface that goes from our system 45 | to the outside world (I/O).__ 46 | 47 | | Repository | Abstraction around persistent storage. Each aggregate has its own repository. 48 | | Event publisher | Pushes events onto the external message bus. 49 | 50 | .2+a| *Entrypoints* (Primary adapters) 51 | 52 | __Translate external inputs into calls into the service layer.__ 53 | 54 | | Web | Receives web requests and translates them into commands, passing them to the internal message bus. 55 | | Event consumer | Reads events from the external message bus and translates them into commands, passing them to the internal message bus. 56 | 57 | | N/A | External message bus (message broker) | A piece of infrastructure that different services use to intercommunicate, via events. 58 | |=== 59 | ((("architecture, summary diagram and table", startref="ix_archsumm"))) 60 | -------------------------------------------------------------------------------- /appendix_project_structure.asciidoc: -------------------------------------------------------------------------------- 1 | [[appendix_project_structure]] 2 | [appendix] 3 | == A Template Project Structure 4 | 5 | ((("projects", "template project structure", id="ix_prjstrct"))) 6 | Around <<chapter_04_service_layer>>, we moved from just having 7 | everything in one folder to a more structured tree, and we thought it might 8 | be of interest to outline the moving parts. 9 | 10 | [TIP] 11 | ==== 12 | The code for this appendix is in the 13 | appendix_project_structure branch https://oreil.ly/1rDRC[on GitHub]: 14 | 15 | ---- 16 | git clone https://github.com/cosmicpython/code.git 17 | cd code 18 | git checkout appendix_project_structure 19 | ---- 20 | ==== 21 | 22 | 23 | The basic folder structure looks like this: 24 | 25 | [[project_tree]] 26 | .Project tree 27 | ==== 28 | [source,text] 29 | [role="tree"] 30 | ---- 31 | . 32 | ├── Dockerfile <1> 33 | ├── Makefile <2> 34 | ├── README.md 35 | ├── docker-compose.yml <1> 36 | ├── license.txt 37 | ├── mypy.ini 38 | ├── requirements.txt 39 | ├── src <3> 40 | │ ├── allocation 41 | │ │ ├── __init__.py 42 | │ │ ├── adapters 43 | │ │ │ ├── __init__.py 44 | │ │ │ ├── orm.py 45 | │ │ │ └── repository.py 46 | │ │ ├── config.py 47 | │ │ ├── domain 48 | │ │ │ ├── __init__.py 49 | │ │ │ └── model.py 50 | │ │ ├── entrypoints 51 | │ │ │ ├── __init__.py 52 | │ │ │ └── flask_app.py 53 | │ │ └── service_layer 54 | │ │ ├── __init__.py 55 | │ │ └── services.py 56 | │ └── setup.py <3> 57 | └── tests <4> 58 | ├── conftest.py <4> 59 | ├── e2e 60 | │ └── test_api.py 61 | ├── integration 62 | │ ├── test_orm.py 63 | │ └── test_repository.py 64 | ├── pytest.ini <4> 65 | └── unit 66 | ├── test_allocate.py 67 | ├── test_batches.py 68 | └── test_services.py 69 | ---- 70 | ==== 71 | 72 | <1> Our _docker-compose.yml_ and our _Dockerfile_ are the main bits of configuration 73 | for the containers that run our app, and they can also run the tests (for CI). A 74 | more complex project might have several Dockerfiles, although we've found that 75 | minimizing the number of images is usually a good idea.footnote:[Splitting 76 | out images for production and testing is sometimes a good idea, but we've tended 77 | to find that going further and trying to split out different images for 78 | different types of application code (e.g., Web API versus pub/sub client) usually 79 | ends up being more trouble than it's worth; the cost in terms of complexity 80 | and longer rebuild/CI times is too high. YMMV.] 81 | 82 | <2> A __Makefile__ provides the entrypoint for all the typical commands a developer 83 | (or a CI server) might want to run during their normal workflow: `make 84 | build`, `make test`, and so on.footnote:[A pure-Python alternative to Makefiles is 85 | http://www.pyinvoke.org[Invoke], worth checking out if everyone on your 86 | team knows Python (or at least knows it better than Bash!).] This is optional. You could just use 87 | `docker-compose` and `pytest` directly, but if nothing else, it's nice to 88 | have all the "common commands" in a list somewhere, and unlike 89 | documentation, a Makefile is code so it has less tendency to become out of date. 90 | 91 | <3> All the source code for our app, including the domain model, the 92 | Flask app, and infrastructure code, lives in a Python package inside 93 | _src_,footnote:[https://hynek.me/articles/testing-packaging["Testing and Packaging"] by Hynek Schlawack provides more information on _src_ folders.] 94 | which we install using `pip install -e` and the _setup.py_ file. This makes 95 | imports easy. Currently, the structure within this module is totally flat, 96 | but for a more complex project, you'd expect to grow a folder hierarchy 97 | that includes _domain_model/_, _infrastructure/_, _services/_, and _api/_. 98 | 99 | 100 | <4> Tests live in their own folder. Subfolders distinguish different test 101 | types and allow you to run them separately. We can keep shared fixtures 102 | (_conftest.py_) in the main tests folder and nest more specific ones if we 103 | wish. This is also the place to keep _pytest.ini_. 104 | 105 | 106 | 107 | TIP: The https://oreil.ly/QVb9Q[pytest docs] are really good on test layout and importability. 108 | 109 | 110 | Let's look at a few of these files and concepts in more detail. 111 | 112 | 113 | 114 | === Env Vars, 12-Factor, and Config, Inside and Outside Containers 115 | 116 | The basic problem we're trying to solve here is that we need different 117 | config settings for the following: 118 | 119 | - Running code or tests directly from your own dev machine, perhaps 120 | talking to mapped ports from Docker containers 121 | 122 | - Running on the containers themselves, with "real" ports and hostnames 123 | 124 | - Different container environments (dev, staging, prod, and so on) 125 | 126 | Configuration through environment variables as suggested by the 127 | https://12factor.net/config[12-factor manifesto] will solve this problem, 128 | but concretely, how do we implement it in our code and our containers? 129 | 130 | 131 | === Config.py 132 | 133 | Whenever our application code needs access to some config, it's going to 134 | get it from a file called __config.py__. Here are a couple of examples from our 135 | app: 136 | 137 | [[config_dot_py]] 138 | .Sample config functions (src/allocation/config.py) 139 | ==== 140 | [source,python] 141 | ---- 142 | import os 143 | 144 | 145 | def get_postgres_uri(): #<1> 146 | host = os.environ.get("DB_HOST", "localhost") #<2> 147 | port = 54321 if host == "localhost" else 5432 148 | password = os.environ.get("DB_PASSWORD", "abc123") 149 | user, db_name = "allocation", "allocation" 150 | return f"postgresql://{user}:{password}@{host}:{port}/{db_name}" 151 | 152 | 153 | def get_api_url(): 154 | host = os.environ.get("API_HOST", "localhost") 155 | port = 5005 if host == "localhost" else 80 156 | return f"http://{host}:{port}" 157 | ---- 158 | ==== 159 | 160 | <1> We use functions for getting the current config, rather than constants 161 | available at import time, because that allows client code to modify 162 | `os.environ` if it needs to. 163 | 164 | <2> _config.py_ also defines some default settings, designed to work when 165 | running the code from the developer's local machine.footnote:[ 166 | This gives us a local development setup that "just works" (as much as possible). 167 | You may prefer to fail hard on missing environment variables instead, particularly 168 | if any of the defaults would be insecure in production.] 169 | 170 | An elegant Python package called 171 | https://github.com/hynek/environ-config[_environ-config_] is worth looking 172 | at if you get tired of hand-rolling your own environment-based config functions. 173 | 174 | 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. 175 | Keep things immutable and modify them only via environment variables. 176 | If you decide to use a <<chapter_13_dependency_injection,bootstrap script>>, 177 | you can make it the only place (other than tests) that config is imported to. 178 | 179 | === Docker-Compose and Containers Config 180 | 181 | We use a lightweight Docker container orchestration tool called _docker-compose_. 182 | It's main configuration is via a YAML file (sigh):footnote:[Harry is a bit YAML-weary. 183 | It's _everywhere_, and yet he can never remember the syntax or how it's supposed 184 | to indent.] 185 | 186 | 187 | [[docker_compose]] 188 | .docker-compose config file (docker-compose.yml) 189 | ==== 190 | [source,yaml] 191 | ---- 192 | version: "3" 193 | services: 194 | 195 | app: #<1> 196 | build: 197 | context: . 198 | dockerfile: Dockerfile 199 | depends_on: 200 | - postgres 201 | environment: #<3> 202 | - DB_HOST=postgres <4> 203 | - DB_PASSWORD=abc123 204 | - API_HOST=app 205 | - PYTHONDONTWRITEBYTECODE=1 #<5> 206 | volumes: #<6> 207 | - ./src:/src 208 | - ./tests:/tests 209 | ports: 210 | - "5005:80" <7> 211 | 212 | 213 | postgres: 214 | image: postgres:9.6 #<2> 215 | environment: 216 | - POSTGRES_USER=allocation 217 | - POSTGRES_PASSWORD=abc123 218 | ports: 219 | - "54321:5432" 220 | ---- 221 | ==== 222 | 223 | <1> In the _docker-compose_ file, we define the different _services_ 224 | (containers) that we need for our app. Usually one main image 225 | contains all our code, and we can use it to run our API, our tests, 226 | or any other service that needs access to the domain model. 227 | 228 | <2> You'll probably have other infrastructure services, including a database. 229 | In production you might not use containers for this; you might have a cloud 230 | provider instead, but _docker-compose_ gives us a way of producing a 231 | similar service for dev or CI. 232 | 233 | <3> The `environment` stanza lets you set the environment variables for your 234 | containers, the hostnames and ports as seen from inside the Docker cluster. 235 | If you have enough containers that information starts to be duplicated in 236 | these sections, you can use `environment_file` instead. We usually call 237 | ours _container.env_. 238 | 239 | <4> Inside a cluster, _docker-compose_ sets up networking such that containers are 240 | available to each other via hostnames named after their service name. 241 | 242 | <5> Pro tip: if you're mounting volumes to share source folders between your 243 | local dev machine and the container, the `PYTHONDONTWRITEBYTECODE` environment variable 244 | tells Python to not write _.pyc_ files, and that will save you from 245 | having millions of root-owned files sprinkled all over your local filesystem, 246 | being all annoying to delete and causing weird Python compiler errors besides. 247 | 248 | <6> Mounting our source and test code as `volumes` means we don't need to rebuild 249 | our containers every time we make a code change. 250 | 251 | <7> The `ports` section allows us to expose the ports from inside the containers 252 | to the outside worldfootnote:[On a CI server, you may not be able to expose 253 | arbitrary ports reliably, but it's only a convenience for local dev. You 254 | can find ways of making these port mappings optional (e.g., with 255 | _docker-compose.override.yml_).]—these correspond to the default ports we set 256 | in _config.py_. 257 | 258 | NOTE: Inside Docker, other containers are available through hostnames named after 259 | their service name. Outside Docker, they are available on `localhost`, at the 260 | port defined in the `ports` section. 261 | 262 | 263 | === Installing Your Source as a Package 264 | 265 | All our application code (everything except tests, really) lives inside an 266 | _src_ folder: 267 | 268 | [[src_folder_tree]] 269 | .The src folder 270 | ==== 271 | [source,text] 272 | [role="skip"] 273 | ---- 274 | ├── src 275 | │ ├── allocation #<1> 276 | │ │ ├── config.py 277 | │ │ └── ... 278 | │ └── setup.py <2> 279 | ---- 280 | ==== 281 | 282 | <1> Subfolders define top-level module names. You can have multiple if you like. 283 | 284 | <2> And _setup.py_ is the file you need to make it pip-installable, shown next. 285 | 286 | [[setup_dot_py]] 287 | .pip-installable modules in three lines (src/setup.py) 288 | ==== 289 | [source,python] 290 | ---- 291 | from setuptools import setup 292 | 293 | setup( 294 | name="allocation", version="0.1", packages=["allocation"], 295 | ) 296 | ---- 297 | ==== 298 | 299 | That's all you need. `packages=` specifies the names of subfolders that you 300 | want to install as top-level modules. The `name` entry is just cosmetic, but 301 | it's required. For a package that's never actually going to hit PyPI, it'll 302 | do fine.footnote:[For more _setup.py_ tips, see 303 | https://oreil.ly/KMWDz[this article on packaging] by Hynek.] 304 | 305 | 306 | === Dockerfile 307 | 308 | Dockerfiles are going to be very project-specific, but here are a few key stages 309 | you'll expect to see: 310 | 311 | [[dockerfile]] 312 | .Our Dockerfile (Dockerfile) 313 | ==== 314 | [source,dockerfile] 315 | ---- 316 | FROM python:3.9-slim-buster 317 | 318 | <1> 319 | # RUN apt install gcc libpq (no longer needed bc we use psycopg2-binary) 320 | 321 | <2> 322 | COPY requirements.txt /tmp/ 323 | RUN pip install -r /tmp/requirements.txt 324 | 325 | <3> 326 | RUN mkdir -p /src 327 | COPY src/ /src/ 328 | RUN pip install -e /src 329 | COPY tests/ /tests/ 330 | 331 | <4> 332 | WORKDIR /src 333 | ENV FLASK_APP=allocation/entrypoints/flask_app.py FLASK_DEBUG=1 PYTHONUNBUFFERED=1 334 | CMD flask run --host=0.0.0.0 --port=80 335 | ---- 336 | ==== 337 | 338 | <1> Installing system-level dependencies 339 | <2> Installing our Python dependencies (you may want to split out your dev from 340 | prod dependencies; we haven't here, for simplicity) 341 | <3> Copying and installing our source 342 | <4> Optionally configuring a default startup command (you'll probably override 343 | this a lot from the command line) 344 | 345 | TIP: One thing to note is that we install things in the order of how frequently they 346 | are likely to change. This allows us to maximize Docker build cache reuse. I 347 | can't tell you how much pain and frustration underlies this lesson. For this 348 | and many more Python Dockerfile improvement tips, check out 349 | https://pythonspeed.com/docker["Production-Ready Docker Packaging"]. 350 | 351 | === Tests 352 | 353 | ((("testing", "tests folder tree"))) 354 | Our tests are kept alongside everything else, as shown here: 355 | 356 | [[tests_folder]] 357 | .Tests folder tree 358 | ==== 359 | [source,text] 360 | [role="tree"] 361 | ---- 362 | └── tests 363 | ├── conftest.py 364 | ├── e2e 365 | │ └── test_api.py 366 | ├── integration 367 | │ ├── test_orm.py 368 | │ └── test_repository.py 369 | ├── pytest.ini 370 | └── unit 371 | ├── test_allocate.py 372 | ├── test_batches.py 373 | └── test_services.py 374 | ---- 375 | ==== 376 | 377 | Nothing particularly clever here, just some separation of different test types 378 | that you're likely to want to run separately, and some files for common fixtures, 379 | config, and so on. 380 | 381 | There's no _src_ folder or _setup.py_ in the test folders because we usually 382 | haven't needed to make tests pip-installable, but if you have difficulties with 383 | import paths, you might find it helps. 384 | 385 | 386 | === Wrap-Up 387 | 388 | These are our basic building blocks: 389 | 390 | * Source code in an _src_ folder, pip-installable using _setup.py_ 391 | * Some Docker config for spinning up a local cluster that mirrors production as far as possible 392 | * Configuration via environment variables, centralized in a Python file called _config.py_, with defaults allowing things to run _outside_ containers 393 | * A Makefile for useful command-line, um, commands 394 | 395 | ((("projects", "template project structure", startref="ix_prjstrct"))) 396 | We doubt that anyone will end up with _exactly_ the same solutions we did, but we hope you 397 | find some inspiration here. 398 | -------------------------------------------------------------------------------- /atlas.json: -------------------------------------------------------------------------------- 1 | { 2 | "branch": "master", 3 | "files": [ 4 | "cover.html", 5 | "titlepage.html", 6 | "copyright.html", 7 | "toc.html", 8 | "preface.asciidoc", 9 | "introduction.asciidoc", 10 | "part1.asciidoc", 11 | "chapter_01_domain_model.asciidoc", 12 | "chapter_02_repository.asciidoc", 13 | "chapter_03_abstractions.asciidoc", 14 | "chapter_04_service_layer.asciidoc", 15 | "chapter_05_high_gear_low_gear.asciidoc", 16 | "chapter_06_uow.asciidoc", 17 | "chapter_07_aggregate.asciidoc", 18 | "part2.asciidoc", 19 | "chapter_08_events_and_message_bus.asciidoc", 20 | "chapter_09_all_messagebus.asciidoc", 21 | "chapter_10_commands.asciidoc", 22 | "chapter_11_external_events.asciidoc", 23 | "chapter_12_cqrs.asciidoc", 24 | "chapter_13_dependency_injection.asciidoc", 25 | "epilogue_1_how_to_get_there_from_here.asciidoc", 26 | "appendix_ds1_table.asciidoc", 27 | "appendix_project_structure.asciidoc", 28 | "appendix_csvs.asciidoc", 29 | "appendix_django.asciidoc", 30 | "appendix_validation.asciidoc", 31 | "ix.html", 32 | "author_bio.html", 33 | "colo.html" 34 | ], 35 | "formats": { 36 | "pdf": { 37 | "version": "web", 38 | "color_count": "4", 39 | "index": true, 40 | "toc": true, 41 | "syntaxhighlighting": true, 42 | "show_comments": false, 43 | "trim_size": "7inx9.1875in", 44 | "antennahouse_version": "AHFormatterV62_64-MR4" 45 | }, 46 | "epub": { 47 | "index": true, 48 | "toc": true, 49 | "epubcheck": true, 50 | "syntaxhighlighting": true, 51 | "show_comments": false, 52 | "downsample_images": false, 53 | "mathmlreplacement": false 54 | }, 55 | "mobi": { 56 | "index": true, 57 | "toc": true, 58 | "syntaxhighlighting": true, 59 | "show_comments": false, 60 | "downsample_images": false 61 | }, 62 | "html": { 63 | "index": true, 64 | "toc": true, 65 | "syntaxhighlighting": true, 66 | "show_comments": false, 67 | "consolidated": false 68 | } 69 | }, 70 | "theme": "oreillymedia/animal_theme_sass", 71 | "title": "Architecture Patterns with Python", 72 | "print_isbn13": "9781492052203", 73 | "lang": "en", 74 | "accent_color": "", 75 | "templating": false 76 | } -------------------------------------------------------------------------------- /author_bio.html: -------------------------------------------------------------------------------- 1 | <section data-type="colophon" xmlns="http://www.w3.org/1999/xhtml" class="abouttheauthor"> 2 | <h1>About the Authors</h1> 3 | <p><strong>Harry Percival</strong> spent a few years being deeply unhappy as a management consultant. Soon he rediscovered his true geek nature and was lucky enough to fall in with a bunch of XP fanatics, working on pioneering the sadly defunct Resolver One spreadsheet. He worked at PythonAnywhere LLP, spreading the gospel of TDD worldwide at talks, workshops, and conferences. He is now with MADE.com.</p> 4 | 5 | <p><strong>Bob Gregory</strong> is a UK-based software architect with MADE.com. He has been building event-driven systems with domain-driven design for more than a decade.</p> 6 | </section> 7 | -------------------------------------------------------------------------------- /book.asciidoc: -------------------------------------------------------------------------------- 1 | :doctype: book 2 | :source-highlighter: pygments 3 | :icons: font 4 | :toc: left 5 | :toclevels: 1 6 | 7 | = Architecture Patterns with Python 8 | 9 | :sectnums!: 10 | 11 | include::preface.asciidoc[] 12 | 13 | include::introduction.asciidoc[] 14 | 15 | 16 | :sectnums: 17 | 18 | include::part1.asciidoc[] 19 | 20 | include::chapter_01_domain_model.asciidoc[] 21 | 22 | include::chapter_02_repository.asciidoc[] 23 | 24 | include::chapter_03_abstractions.asciidoc[] 25 | 26 | 27 | include::chapter_04_service_layer.asciidoc[] 28 | 29 | include::chapter_05_high_gear_low_gear.asciidoc[] 30 | 31 | include::chapter_06_uow.asciidoc[] 32 | 33 | include::chapter_07_aggregate.asciidoc[] 34 | 35 | 36 | include::part2.asciidoc[] 37 | 38 | include::chapter_08_events_and_message_bus.asciidoc[] 39 | 40 | include::chapter_09_all_messagebus.asciidoc[] 41 | 42 | include::chapter_10_commands.asciidoc[] 43 | 44 | include::chapter_11_external_events.asciidoc[] 45 | 46 | include::chapter_12_cqrs.asciidoc[] 47 | 48 | include::chapter_13_dependency_injection.asciidoc[] 49 | 50 | 51 | :sectnums!: 52 | 53 | include::epilogue_1_how_to_get_there_from_here.asciidoc[] 54 | 55 | :sectnums: 56 | 57 | include::appendix_ds1_table.asciidoc[] 58 | 59 | include::appendix_project_structure.asciidoc[] 60 | 61 | include::appendix_csvs.asciidoc[] 62 | 63 | include::appendix_django.asciidoc[] 64 | 65 | include::appendix_validation.asciidoc[] 66 | 67 | -------------------------------------------------------------------------------- /callouts/1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/1.pdf -------------------------------------------------------------------------------- /callouts/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/1.png -------------------------------------------------------------------------------- /callouts/10.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/10.pdf -------------------------------------------------------------------------------- /callouts/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/10.png -------------------------------------------------------------------------------- /callouts/11.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/11.pdf -------------------------------------------------------------------------------- /callouts/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/11.png -------------------------------------------------------------------------------- /callouts/12.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/12.pdf -------------------------------------------------------------------------------- /callouts/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/12.png -------------------------------------------------------------------------------- /callouts/13.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/13.pdf -------------------------------------------------------------------------------- /callouts/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/13.png -------------------------------------------------------------------------------- /callouts/14.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/14.pdf -------------------------------------------------------------------------------- /callouts/14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/14.png -------------------------------------------------------------------------------- /callouts/15.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/15.pdf -------------------------------------------------------------------------------- /callouts/15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/15.png -------------------------------------------------------------------------------- /callouts/16.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/16.pdf -------------------------------------------------------------------------------- /callouts/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/16.png -------------------------------------------------------------------------------- /callouts/17.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/17.pdf -------------------------------------------------------------------------------- /callouts/17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/17.png -------------------------------------------------------------------------------- /callouts/18.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/18.pdf -------------------------------------------------------------------------------- /callouts/18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/18.png -------------------------------------------------------------------------------- /callouts/19.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/19.pdf -------------------------------------------------------------------------------- /callouts/19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/19.png -------------------------------------------------------------------------------- /callouts/2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/2.pdf -------------------------------------------------------------------------------- /callouts/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/2.png -------------------------------------------------------------------------------- /callouts/20.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/20.pdf -------------------------------------------------------------------------------- /callouts/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/20.png -------------------------------------------------------------------------------- /callouts/21.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/21.pdf -------------------------------------------------------------------------------- /callouts/21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/21.png -------------------------------------------------------------------------------- /callouts/22.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/22.pdf -------------------------------------------------------------------------------- /callouts/22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/22.png -------------------------------------------------------------------------------- /callouts/23.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/23.pdf -------------------------------------------------------------------------------- /callouts/23.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/23.png -------------------------------------------------------------------------------- /callouts/24.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/24.pdf -------------------------------------------------------------------------------- /callouts/24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/24.png -------------------------------------------------------------------------------- /callouts/25.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/25.pdf -------------------------------------------------------------------------------- /callouts/25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/25.png -------------------------------------------------------------------------------- /callouts/26.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/26.pdf -------------------------------------------------------------------------------- /callouts/26.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/26.png -------------------------------------------------------------------------------- /callouts/27.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/27.pdf -------------------------------------------------------------------------------- /callouts/27.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/27.png -------------------------------------------------------------------------------- /callouts/28.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/28.pdf -------------------------------------------------------------------------------- /callouts/28.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/28.png -------------------------------------------------------------------------------- /callouts/29.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/29.pdf -------------------------------------------------------------------------------- /callouts/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/29.png -------------------------------------------------------------------------------- /callouts/3.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/3.pdf -------------------------------------------------------------------------------- /callouts/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/3.png -------------------------------------------------------------------------------- /callouts/30.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/30.pdf -------------------------------------------------------------------------------- /callouts/30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/30.png -------------------------------------------------------------------------------- /callouts/31.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/31.pdf -------------------------------------------------------------------------------- /callouts/31.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/31.png -------------------------------------------------------------------------------- /callouts/32.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/32.pdf -------------------------------------------------------------------------------- /callouts/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/32.png -------------------------------------------------------------------------------- /callouts/33.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/33.pdf -------------------------------------------------------------------------------- /callouts/33.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/33.png -------------------------------------------------------------------------------- /callouts/34.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/34.pdf -------------------------------------------------------------------------------- /callouts/34.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/34.png -------------------------------------------------------------------------------- /callouts/35.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/35.pdf -------------------------------------------------------------------------------- /callouts/35.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/35.png -------------------------------------------------------------------------------- /callouts/36.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/36.pdf -------------------------------------------------------------------------------- /callouts/36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/36.png -------------------------------------------------------------------------------- /callouts/37.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/37.pdf -------------------------------------------------------------------------------- /callouts/37.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/37.png -------------------------------------------------------------------------------- /callouts/38.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/38.pdf -------------------------------------------------------------------------------- /callouts/38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/38.png -------------------------------------------------------------------------------- /callouts/39.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/39.pdf -------------------------------------------------------------------------------- /callouts/39.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/39.png -------------------------------------------------------------------------------- /callouts/4.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/4.pdf -------------------------------------------------------------------------------- /callouts/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/4.png -------------------------------------------------------------------------------- /callouts/5.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/5.pdf -------------------------------------------------------------------------------- /callouts/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/5.png -------------------------------------------------------------------------------- /callouts/6.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/6.pdf -------------------------------------------------------------------------------- /callouts/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/6.png -------------------------------------------------------------------------------- /callouts/7.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/7.pdf -------------------------------------------------------------------------------- /callouts/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/7.png -------------------------------------------------------------------------------- /callouts/8.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/8.pdf -------------------------------------------------------------------------------- /callouts/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/8.png -------------------------------------------------------------------------------- /callouts/9.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/9.pdf -------------------------------------------------------------------------------- /callouts/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/callouts/9.png -------------------------------------------------------------------------------- /chapters.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | CHAPTERS = [ 4 | 'chapter_01_domain_model', 5 | 'chapter_02_repository', 6 | "chapter_04_service_layer", 7 | "chapter_05_high_gear_low_gear", 8 | "appendix_project_structure", 9 | 'appendix_django', 10 | "chapter_06_uow", 11 | "appendix_csvs", 12 | "chapter_07_aggregate", 13 | "chapter_08_events_and_message_bus", 14 | "chapter_09_all_messagebus", 15 | "chapter_10_commands", 16 | "chapter_11_external_events", 17 | "chapter_12_cqrs", 18 | "chapter_13_dependency_injection", 19 | ] 20 | 21 | BRANCHES = { 22 | 'appendix_csvs', 23 | 'appendix_django', 24 | } 25 | 26 | STANDALONE = [ 27 | 'chapter_03_abstractions', 28 | ] 29 | 30 | NO_EXERCISE = [ 31 | "chapter_03_abstractions", 32 | "chapter_05_high_gear_low_gear", 33 | "appendix_project_structure", 34 | 'appendix_django', 35 | "appendix_csvs", 36 | "chapter_09_all_messagebus", 37 | "chapter_10_commands", 38 | "chapter_11_external_events", 39 | "chapter_12_cqrs", 40 | "chapter_13_dependency_injection", 41 | ] 42 | 43 | if __name__ == "__main__": 44 | print("\n".join(CHAPTERS + STANDALONE)) 45 | -------------------------------------------------------------------------------- /checkout-branches-for-ci.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import subprocess 4 | from pathlib import Path 5 | from chapters import CHAPTERS, STANDALONE, NO_EXERCISE 6 | 7 | cwd = Path(__file__).parent / 'code' 8 | 9 | for chap in CHAPTERS + STANDALONE: 10 | subprocess.run(['git', 'checkout', chap], cwd=cwd, check=True) 11 | if chap in NO_EXERCISE: 12 | continue 13 | subprocess.run(['git', 'checkout', f'{chap}_exercise'], cwd=cwd, check=True) 14 | 15 | subprocess.run(['git', 'checkout', 'master'], cwd=cwd, check=True) 16 | -------------------------------------------------------------------------------- /colo.html: -------------------------------------------------------------------------------- 1 | <section id="colophon" data-type="colophon" xmlns="http://www.w3.org/1999/xhtml"> 2 | <h1>Colophon</h1> 3 | 4 | <p>The animal on the cover of <em>Architecture Patterns with Python</em> is a Burmese python (<em>Python bivitattus</em>). As you might expect, the Burmese python is native to Southeast Asia. Today it lives in jungles and marshes in South Asia, Myanmar, China, and Indonesia; it’s also invasive in Florida’s Everglades.</p> 5 | 6 | <p>Burmese pythons are one of the world’s largest species of snakes. These nocturnal, carnivorous constrictors can grow to 23 feet and 200 pounds. Females are larger than males. They can lay up to a hundred eggs in one clutch. In the wild, Burmese pythons live an average of 20 to 25 years.</p> 7 | 8 | <p>The markings on a Burmese python begin with an arrow-shaped spot of light brown on top of the head and continue along the body in rectangles that stand out against its otherwise tan scales. Before they reach their full size, which takes two to three years, Burmese pythons live in trees hunting small mammals and birds. They also swim for long stretches of time—going up to 30 minutes without air.</p> 9 | 10 | <p>Because of habitat destruction, the Burmese python has a conservation status of Vulnerable. Many of the animals on O’Reilly’s covers are endangered; all of them are important to the world.</p> 11 | 12 | <p>The color illustration is by Jose Marzan, based on a black-and-white engraving from <em>Encyclopedie D'Histoire Naturelle</em>. The cover fonts are URW Typewriter and Guardian Sans. The text font is Adobe Minion Pro; the heading font is Adobe Myriad Condensed; and the code font is Dalton Maag's Ubuntu Mono.</p> 13 | </section> 14 | -------------------------------------------------------------------------------- /copyright.html: -------------------------------------------------------------------------------- 1 | <section data-type="copyright-page" xmlns="http://www.w3.org/1999/xhtml"> 2 | <h1>Architecture Patterns with Python</h1> 3 | 4 | <p class="author">by <span class="firstname">Harry </span> <span class="surname">Percival</span> and <span class="firstname">Bob </span> <span class="surname">Gregory</span></p> 5 | 6 | <p class="copyright">Copyright © 2020 Harry Percival and Bob Gregory. All rights reserved.</p> 7 | 8 | <p class="printlocation">Printed in the United States of America.</p> 9 | 10 | <p class="publisher">Published by <span class="publishername">O'Reilly Media, Inc.</span>, 1005 Gravenstein Highway North, Sebastopol, CA 95472.</p> 11 | 12 | <p>O'Reilly books may be purchased for educational, business, or sales promotional use. Online editions are also available for most titles (<a href="http://oreilly.com">http://oreilly.com</a>). For more information, contact our corporate/institutional sales department: 800-998-9938 or <span data-type="email"><em>corporate@oreilly.com</em></span>.</p> 13 | 14 | <ul class="stafflist"> 15 | <li><span class="staffrole">Acquisitions Editor:</span> Ryan Shaw</li> 16 | <li><span class="staffrole">Development Editor:</span> Corbin Collins</li> 17 | <li><span class="staffrole">Production Editor:</span> Katherine Tozer</li> 18 | <li><span class="staffrole">Copyeditor:</span> Sharon Wilkey</li> 19 | <li><span class="staffrole">Proofreader:</span> Arthur Johnson</li> 20 | <li><span class="staffrole">Indexer:</span> Ellen Troutman-Zaig</li> 21 | <li><span class="staffrole">Interior Designer:</span> David Futato</li> 22 | <li><span class="staffrole">Cover Designer:</span> Karen Montgomery</li> 23 | <li><span class="staffrole">Illustrator:</span> Rebecca Demarest</li> 24 | </ul> 25 | 26 | <ul class="printings"> 27 | <li><span class="printedition">March 2020:</span> First Edition</li> 28 | </ul> 29 | <!--Add additional revdate spans below as needed.--> 30 | 31 | <div> 32 | <h1 class="revisions">Revision History for the First Edition</h1> 33 | 34 | <ul class="releases"> 35 | <li><span class="revdate">2020-03-05:</span> First Release</li> 36 | </ul> 37 | </div> 38 | 39 | <p class="errata">See <a href="http://oreilly.com/catalog/errata.csp?isbn=9781492052203">http://oreilly.com/catalog/errata.csp?isbn=9781492052203</a> for release details.</p> 40 | 41 | <div class="legal"> 42 | <p>The O’Reilly logo is a registered trademark of O’Reilly Media, Inc. <em>Architecture Patterns with Python</em>, the cover image, and related trade dress are trademarks of O’Reilly Media, Inc.</p> 43 | 44 | <p>The views expressed in this work are those of the authors and do not represent the publisher's views. While the publisher and the authors have used good faith efforts to ensure that the information and instructions contained in this work are accurate, the publisher and the authors disclaim all responsibility for errors or omissions, including without limitation responsibility for damages resulting from the use of or reliance on this work. Use of the information and instructions contained in this work is at your own risk. If any code samples or other technology this work contains or describes is subject to open source licenses or the intellectual property rights of others, it is your responsibility to ensure that your use thereof complies with such licenses and/or rights. <!--PROD: Uncomment the following sentence if appropriate and add it to the 45 | above para:--> <!--This book is not intended as [legal/medical/financial; use the appropriate 46 | reference] advice. Please consult a qualified professional if you 47 | require [legal/medical/financial] advice.--></p> 48 | </div> 49 | 50 | <div class="copyright-bottom"> 51 | <p class="isbn">978-1-492-05220-3</p> 52 | 53 | <p class="printer">[LSI]</p> 54 | </div> 55 | </section> 56 | -------------------------------------------------------------------------------- /cover.html: -------------------------------------------------------------------------------- 1 | <figure data-type="cover"> 2 | <img src="images/cover.png"/> 3 | </figure> -------------------------------------------------------------------------------- /fix-branches.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import subprocess 4 | from pathlib import Path 5 | from chapters import CHAPTERS 6 | 7 | for chapter in CHAPTERS: 8 | subprocess.run( 9 | ['git', '-c', 'pager.show=false', 'show', '-s', '--oneline', f':/{chapter}_ends'], 10 | cwd=Path(__file__).parent / 'code' 11 | ) 12 | subprocess.run( 13 | ['git', 'branch', '-f', chapter, f':/{chapter}_ends'], 14 | cwd=Path(__file__).parent / 'code' 15 | ) 16 | subprocess.run( 17 | ['git', 'diff', f'origin/{chapter}', chapter], 18 | cwd=Path(__file__).parent / 'code' 19 | ) 20 | -------------------------------------------------------------------------------- /images/C4.puml: -------------------------------------------------------------------------------- 1 | ' C4-PlantUML, version 1.0.0 2 | ' https://github.com/RicardoNiepel/C4-PlantUML 3 | 4 | ' Colors 5 | ' ################################## 6 | 7 | !define ELEMENT_FONT_COLOR #FFFFFF 8 | 9 | ' Styling 10 | ' ################################## 11 | 12 | !define TECHN_FONT_SIZE 18 13 | 14 | skinparam roundCorner 20 15 | skinparam Padding 2 16 | skinparam wrapWidth 200 17 | skinparam default { 18 | FontName Guardian Sans Cond Regular 19 | FontSize 18 20 | } 21 | 22 | skinparam defaultTextAlignment center 23 | 24 | skinparam wrapWidth 200 25 | skinparam maxMessageSize 150 26 | 27 | skinparam rectangle { 28 | StereotypeFontSize 18 29 | shadowing false 30 | } 31 | 32 | skinparam database { 33 | StereotypeFontSize 18 34 | shadowing false 35 | } 36 | 37 | skinparam Arrow { 38 | Color #666666 39 | FontColor #666666 40 | FontSize 18 41 | } 42 | 43 | skinparam rectangle<<boundary>> { 44 | Shadowing false 45 | StereotypeFontSize 0 46 | FontColor #444444 47 | BorderColor #444444 48 | BorderStyle dashed 49 | } 50 | 51 | ' Layout 52 | ' ################################## 53 | 54 | !definelong LAYOUT_AS_SKETCH 55 | skinparam backgroundColor #EEEBDC 56 | skinparam handwritten true 57 | skinparam defaultFontName "Comic Sans MS" 58 | center footer <font color=red>Warning:</font> Created for discussion, needs to be validated 59 | !enddefinelong 60 | 61 | !define LAYOUT_TOP_DOWN top to bottom direction 62 | !define LAYOUT_LEFT_RIGHT left to right direction 63 | 64 | ' Boundaries 65 | ' ################################## 66 | 67 | !define Boundary(e_alias, e_label) rectangle "==e_label" <<boundary>> as e_alias 68 | !define Boundary(e_alias, e_label, e_type) rectangle "==e_label\n<size:TECHN_FONT_SIZE>[e_type]</size>" <<boundary>> as e_alias 69 | 70 | ' Relationship 71 | ' ################################## 72 | 73 | !define Rel_(e_alias1, e_alias2, e_label, e_direction="") e_alias1 e_direction e_alias2 : "===e_label" 74 | !define Rel_(e_alias1, e_alias2, e_label, e_techn, e_direction="") e_alias1 e_direction e_alias2 : "===e_label\n//<size:TECHN_FONT_SIZE>[e_techn]</size>//" 75 | 76 | !define Rel(e_from,e_to, e_label) Rel_(e_from,e_to, e_label, "-->") 77 | !define Rel(e_from,e_to, e_label, e_techn) Rel_(e_from,e_to, e_label, e_techn, "-->") 78 | 79 | !define Rel_Back(e_to, e_from, e_label) Rel_(e_to, e_from, e_label, "<--") 80 | !define Rel_Back(e_to, e_from, e_label, e_techn) Rel_(e_to, e_from, e_label, e_techn, "<--") 81 | 82 | !define Rel_Neighbor(e_from,e_to, e_label) Rel_(e_from,e_to, e_label, "->") 83 | !define Rel_Neighbor(e_from,e_to, e_label, e_techn) Rel_(e_from,e_to, e_label, e_techn, "->") 84 | 85 | !define Rel_Back_Neighbor(e_to, e_from, e_label) Rel_(e_to, e_from, e_label, "<-") 86 | !define Rel_Back_Neighbor(e_to, e_from, e_label, e_techn) Rel_(e_to, e_from, e_label, e_techn, "<-") 87 | 88 | !define Rel_D(e_from,e_to, e_label) Rel_(e_from,e_to, e_label, "-DOWN->") 89 | !define Rel_D(e_from,e_to, e_label, e_techn) Rel_(e_from,e_to, e_label, e_techn, "-DOWN->") 90 | !define Rel_Down(e_from,e_to, e_label) Rel_D(e_from,e_to, e_label) 91 | !define Rel_Down(e_from,e_to, e_label, e_techn) Rel_D(e_from,e_to, e_label, e_techn) 92 | 93 | !define Rel_U(e_from,e_to, e_label) Rel_(e_from,e_to, e_label, "-UP->") 94 | !define Rel_U(e_from,e_to, e_label, e_techn) Rel_(e_from,e_to, e_label, e_techn, "-UP->") 95 | !define Rel_Up(e_from,e_to, e_label) Rel_U(e_from,e_to, e_label) 96 | !define Rel_Up(e_from,e_to, e_label, e_techn) Rel_U(e_from,e_to, e_label, e_techn) 97 | 98 | !define Rel_L(e_from,e_to, e_label) Rel_(e_from,e_to, e_label, "-LEFT->") 99 | !define Rel_L(e_from,e_to, e_label, e_techn) Rel_(e_from,e_to, e_label, e_techn, "-LEFT->") 100 | !define Rel_Left(e_from,e_to, e_label) Rel_L(e_from,e_to, e_label) 101 | !define Rel_Left(e_from,e_to, e_label, e_techn) Rel_L(e_from,e_to, e_label, e_techn) 102 | 103 | !define Rel_R(e_from,e_to, e_label) Rel_(e_from,e_to, e_label, "-RIGHT->") 104 | !define Rel_R(e_from,e_to, e_label, e_techn) Rel_(e_from,e_to, e_label, e_techn, "-RIGHT->") 105 | !define Rel_Right(e_from,e_to, e_label) Rel_R(e_from,e_to, e_label) 106 | !define Rel_Right(e_from,e_to, e_label, e_techn) Rel_R(e_from,e_to, e_label, e_techn) 107 | 108 | ' Layout Helpers 109 | ' ################################## 110 | 111 | !define Lay_D(e_from, e_to) e_from -[hidden]D- e_to 112 | !define Lay_U(e_from, e_to) e_from -[hidden]U- e_to 113 | !define Lay_R(e_from, e_to) e_from -[hidden]R- e_to 114 | !define Lay_L(e_from, e_to) e_from -[hidden]L- e_to 115 | -------------------------------------------------------------------------------- /images/C4_Component.puml: -------------------------------------------------------------------------------- 1 | ' !includeurl https://raw.githubusercontent.com/RicardoNiepel/C4-PlantUML/master/C4_Container.puml 2 | ' uncomment the following line and comment the first to use locally 3 | !include C4_Container.puml 4 | 5 | ' Scope: A single container. 6 | ' Primary elements: Components within the container in scope. 7 | ' Supporting elements: Containers (within the software system in scope) plus people and software systems directly connected to the components. 8 | ' Intended audience: Software architects and developers. 9 | 10 | ' Colors 11 | ' ################################## 12 | 13 | !define COMPONENT_BG_COLOR #85BBF0 14 | 15 | ' Styling 16 | ' ################################## 17 | 18 | skinparam rectangle<<component>> { 19 | StereotypeFontColor ELEMENT_FONT_COLOR 20 | FontColor #000000 21 | BackgroundColor COMPONENT_BG_COLOR 22 | BorderColor #78A8D8 23 | } 24 | 25 | skinparam database<<component>> { 26 | StereotypeFontColor ELEMENT_FONT_COLOR 27 | FontColor #000000 28 | BackgroundColor COMPONENT_BG_COLOR 29 | BorderColor #78A8D8 30 | } 31 | 32 | ' Layout 33 | ' ################################## 34 | 35 | !definelong LAYOUT_WITH_LEGEND 36 | hide stereotype 37 | legend right 38 | |= |= Type | 39 | |<PERSON_BG_COLOR> | person | 40 | |<EXTERNAL_PERSON_BG_COLOR> | external person | 41 | |<SYSTEM_BG_COLOR> | system | 42 | |<EXTERNAL_SYSTEM_BG_COLOR> | external system | 43 | |<CONTAINER_BG_COLOR> | container | 44 | |<COMPONENT_BG_COLOR> | component | 45 | endlegend 46 | !enddefinelong 47 | 48 | ' Elements 49 | ' ################################## 50 | 51 | !define Component(e_alias, e_label, e_techn) rectangle "==e_label\n//<size:TECHN_FONT_SIZE>[e_techn]</size>//" <<component>> as e_alias 52 | !define Component(e_alias, e_label, e_techn, e_descr) rectangle "==e_label\n//<size:TECHN_FONT_SIZE>[e_techn]</size>//\n\n e_descr" <<component>> as e_alias 53 | 54 | !define ComponentDb(e_alias, e_label, e_techn) database "==e_label\n//<size:TECHN_FONT_SIZE>[e_techn]</size>//" <<component>> as e_alias 55 | !define ComponentDb(e_alias, e_label, e_techn, e_descr) database "==e_label\n//<size:TECHN_FONT_SIZE>[e_techn]</size>//\n\n e_descr" <<component>> as e_alias 56 | -------------------------------------------------------------------------------- /images/C4_Container.puml: -------------------------------------------------------------------------------- 1 | ' !includeurl https://raw.githubusercontent.com/RicardoNiepel/C4-PlantUML/master/C4_Context.puml 2 | ' uncomment the following line and comment the first to use locally 3 | !include C4_Context.puml 4 | 5 | ' Scope: A single software system. 6 | ' Primary elements: Containers within the software system in scope. 7 | ' Supporting elements: People and software systems directly connected to the containers. 8 | ' Intended audience: Technical people inside and outside of the software development team; including software architects, developers and operations/support staff. 9 | 10 | ' Colors 11 | ' ################################## 12 | 13 | !define CONTAINER_BG_COLOR #438DD5 14 | 15 | ' Styling 16 | ' ################################## 17 | 18 | skinparam rectangle<<container>> { 19 | StereotypeFontColor ELEMENT_FONT_COLOR 20 | FontColor ELEMENT_FONT_COLOR 21 | BackgroundColor CONTAINER_BG_COLOR 22 | BorderColor #3C7FC0 23 | } 24 | 25 | skinparam database<<container>> { 26 | StereotypeFontColor ELEMENT_FONT_COLOR 27 | FontColor ELEMENT_FONT_COLOR 28 | BackgroundColor CONTAINER_BG_COLOR 29 | BorderColor #3C7FC0 30 | } 31 | 32 | ' Layout 33 | ' ################################## 34 | 35 | !definelong LAYOUT_WITH_LEGEND 36 | hide stereotype 37 | legend right 38 | |= |= Type | 39 | |<PERSON_BG_COLOR> | person | 40 | |<EXTERNAL_PERSON_BG_COLOR> | external person | 41 | |<SYSTEM_BG_COLOR> | system | 42 | |<EXTERNAL_SYSTEM_BG_COLOR> | external system | 43 | |<CONTAINER_BG_COLOR> | container | 44 | endlegend 45 | !enddefinelong 46 | 47 | ' Elements 48 | ' ################################## 49 | 50 | !define Container(e_alias, e_label, e_techn) rectangle "==e_label\n//<size:TECHN_FONT_SIZE>[e_techn]</size>//" <<container>> as e_alias 51 | !define Container(e_alias, e_label, e_techn, e_descr) rectangle "==e_label\n//<size:TECHN_FONT_SIZE>[e_techn]</size>//\n\n e_descr" <<container>> as e_alias 52 | 53 | !define ContainerDb(e_alias, e_label, e_techn) database "==e_label\n//<size:TECHN_FONT_SIZE>[e_techn]</size>//" <<container>> as e_alias 54 | !define ContainerDb(e_alias, e_label, e_techn, e_descr) database "==e_label\n//<size:TECHN_FONT_SIZE>[e_techn]</size>//\n\n e_descr" <<container>> as e_alias 55 | 56 | ' Boundaries 57 | ' ################################## 58 | 59 | !define Container_Boundary(e_alias, e_label) Boundary(e_alias, e_label, "Container") -------------------------------------------------------------------------------- /images/C4_Context.puml: -------------------------------------------------------------------------------- 1 | ' !includeurl https://raw.githubusercontent.com/RicardoNiepel/C4-PlantUML/master/C4.puml 2 | ' uncomment the following line and comment the first to use locally 3 | !include C4.puml 4 | 5 | ' Scope: A single software system. 6 | ' Primary elements: The software system in scope. 7 | ' Supporting elements: People and software systems directly connected to the software system in scope. 8 | ' Intended audience: Everybody, both technical and non-technical people, inside and outside of the software development team. 9 | 10 | ' Colors 11 | ' ################################## 12 | 13 | !define PERSON_BG_COLOR #08427B 14 | !define EXTERNAL_PERSON_BG_COLOR #686868 15 | !define SYSTEM_BG_COLOR #1168BD 16 | !define EXTERNAL_SYSTEM_BG_COLOR #999999 17 | 18 | ' Styling 19 | ' ################################## 20 | 21 | skinparam rectangle<<person>> { 22 | StereotypeFontColor ELEMENT_FONT_COLOR 23 | FontColor ELEMENT_FONT_COLOR 24 | BackgroundColor PERSON_BG_COLOR 25 | BorderColor #073B6F 26 | } 27 | 28 | skinparam rectangle<<external_person>> { 29 | StereotypeFontColor ELEMENT_FONT_COLOR 30 | FontColor ELEMENT_FONT_COLOR 31 | BackgroundColor EXTERNAL_PERSON_BG_COLOR 32 | BorderColor #8A8A8A 33 | } 34 | 35 | skinparam rectangle<<system>> { 36 | StereotypeFontColor ELEMENT_FONT_COLOR 37 | FontColor ELEMENT_FONT_COLOR 38 | BackgroundColor SYSTEM_BG_COLOR 39 | BorderColor #3C7FC0 40 | } 41 | 42 | skinparam rectangle<<external_system>> { 43 | StereotypeFontColor ELEMENT_FONT_COLOR 44 | FontColor ELEMENT_FONT_COLOR 45 | BackgroundColor EXTERNAL_SYSTEM_BG_COLOR 46 | BorderColor #8A8A8A 47 | } 48 | 49 | skinparam database<<system>> { 50 | StereotypeFontColor ELEMENT_FONT_COLOR 51 | FontColor ELEMENT_FONT_COLOR 52 | BackgroundColor SYSTEM_BG_COLOR 53 | BorderColor #3C7FC0 54 | } 55 | 56 | skinparam database<<external_system>> { 57 | StereotypeFontColor ELEMENT_FONT_COLOR 58 | FontColor ELEMENT_FONT_COLOR 59 | BackgroundColor EXTERNAL_SYSTEM_BG_COLOR 60 | BorderColor #8A8A8A 61 | } 62 | 63 | ' Layout 64 | ' ################################## 65 | 66 | !definelong LAYOUT_WITH_LEGEND 67 | hide stereotype 68 | legend right 69 | |= |= Type | 70 | |<PERSON_BG_COLOR> | person | 71 | |<EXTERNAL_PERSON_BG_COLOR> | external person | 72 | |<SYSTEM_BG_COLOR> | system | 73 | |<EXTERNAL_SYSTEM_BG_COLOR> | external system | 74 | endlegend 75 | !enddefinelong 76 | 77 | ' Elements 78 | ' ################################## 79 | 80 | !define Person(e_alias, e_label) rectangle "==e_label" <<person>> as e_alias 81 | !define Person(e_alias, e_label, e_descr) rectangle "==e_label\n\n e_descr" <<person>> as e_alias 82 | 83 | !define Person_Ext(e_alias, e_label) rectangle "==e_label" <<external_person>> as e_alias 84 | !define Person_Ext(e_alias, e_label, e_descr) rectangle "==e_label\n\n e_descr" <<external_person>> as e_alias 85 | 86 | !define System(e_alias, e_label) rectangle "==e_label" <<system>> as e_alias 87 | !define System(e_alias, e_label, e_descr) rectangle "==e_label\n\n e_descr" <<system>> as e_alias 88 | 89 | !define System_Ext(e_alias, e_label) rectangle "==e_label" <<external_system>> as e_alias 90 | !define System_Ext(e_alias, e_label, e_descr) rectangle "==e_label\n\n e_descr" <<external_system>> as e_alias 91 | 92 | !define SystemDb(e_alias, e_label) database "==e_label" <<system>> as e_alias 93 | !define SystemDb(e_alias, e_label, e_descr) database "==e_label\n\n e_descr" <<system>> as e_alias 94 | 95 | !define SystemDb_Ext(e_alias, e_label) database "==e_label" <<external_system>> as e_alias 96 | !define SystemDb_Ext(e_alias, e_label, e_descr) database "==e_label\n\n e_descr" <<external_system>> as e_alias 97 | 98 | ' Boundaries 99 | ' ################################## 100 | 101 | !define Enterprise_Boundary(e_alias, e_label) Boundary(e_alias, e_label, "Enterprise") 102 | !define System_Boundary(e_alias, e_label) Boundary(e_alias, e_label, "System") 103 | -------------------------------------------------------------------------------- /images/apwp_0001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_0001.png -------------------------------------------------------------------------------- /images/apwp_0002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_0002.png -------------------------------------------------------------------------------- /images/apwp_0101.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_0101.png -------------------------------------------------------------------------------- /images/apwp_0102.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_0102.png -------------------------------------------------------------------------------- /images/apwp_0103.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_0103.png -------------------------------------------------------------------------------- /images/apwp_0104.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_0104.png -------------------------------------------------------------------------------- /images/apwp_0201.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_0201.png -------------------------------------------------------------------------------- /images/apwp_0202.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_0202.png -------------------------------------------------------------------------------- /images/apwp_0203.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_0203.png -------------------------------------------------------------------------------- /images/apwp_0204.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_0204.png -------------------------------------------------------------------------------- /images/apwp_0205.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_0205.png -------------------------------------------------------------------------------- /images/apwp_0206.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_0206.png -------------------------------------------------------------------------------- /images/apwp_0301.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_0301.png -------------------------------------------------------------------------------- /images/apwp_0302.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_0302.png -------------------------------------------------------------------------------- /images/apwp_0401.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_0401.png -------------------------------------------------------------------------------- /images/apwp_0402.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_0402.png -------------------------------------------------------------------------------- /images/apwp_0403.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_0403.png -------------------------------------------------------------------------------- /images/apwp_0404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_0404.png -------------------------------------------------------------------------------- /images/apwp_0405.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_0405.png -------------------------------------------------------------------------------- /images/apwp_0501.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_0501.png -------------------------------------------------------------------------------- /images/apwp_0601.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_0601.png -------------------------------------------------------------------------------- /images/apwp_0602.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_0602.png -------------------------------------------------------------------------------- /images/apwp_0701.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_0701.png -------------------------------------------------------------------------------- /images/apwp_0702.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_0702.png -------------------------------------------------------------------------------- /images/apwp_0703.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_0703.png -------------------------------------------------------------------------------- /images/apwp_0704.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_0704.png -------------------------------------------------------------------------------- /images/apwp_0705.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_0705.png -------------------------------------------------------------------------------- /images/apwp_0801.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_0801.png -------------------------------------------------------------------------------- /images/apwp_0901.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_0901.png -------------------------------------------------------------------------------- /images/apwp_0902.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_0902.png -------------------------------------------------------------------------------- /images/apwp_0903.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_0903.png -------------------------------------------------------------------------------- /images/apwp_0904.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_0904.png -------------------------------------------------------------------------------- /images/apwp_1101.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_1101.png -------------------------------------------------------------------------------- /images/apwp_1102.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_1102.png -------------------------------------------------------------------------------- /images/apwp_1103.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_1103.png -------------------------------------------------------------------------------- /images/apwp_1104.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_1104.png -------------------------------------------------------------------------------- /images/apwp_1105.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_1105.png -------------------------------------------------------------------------------- /images/apwp_1106.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_1106.png -------------------------------------------------------------------------------- /images/apwp_1201.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_1201.png -------------------------------------------------------------------------------- /images/apwp_1202.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_1202.png -------------------------------------------------------------------------------- /images/apwp_1301.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_1301.png -------------------------------------------------------------------------------- /images/apwp_1302.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_1302.png -------------------------------------------------------------------------------- /images/apwp_1303.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_1303.png -------------------------------------------------------------------------------- /images/apwp_aa01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_aa01.png -------------------------------------------------------------------------------- /images/apwp_ep01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_ep01.png -------------------------------------------------------------------------------- /images/apwp_ep02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_ep02.png -------------------------------------------------------------------------------- /images/apwp_ep03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_ep03.png -------------------------------------------------------------------------------- /images/apwp_ep04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_ep04.png -------------------------------------------------------------------------------- /images/apwp_ep05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_ep05.png -------------------------------------------------------------------------------- /images/apwp_ep06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_ep06.png -------------------------------------------------------------------------------- /images/apwp_p101.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_p101.png -------------------------------------------------------------------------------- /images/apwp_p201.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/apwp_p201.png -------------------------------------------------------------------------------- /images/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/images/cover.png -------------------------------------------------------------------------------- /introduction.asciidoc: -------------------------------------------------------------------------------- 1 | [[introduction]] 2 | [preface] 3 | == Introduction 4 | 5 | // TODO (CC): remove "preface" marker from this chapter and check if they renumber correctly 6 | // with this as zero. figures in this chapter should be "Figure 0-1 etc" 7 | 8 | === Why Do Our Designs Go Wrong? 9 | 10 | What comes to mind when you hear the word _chaos?_ Perhaps you think of a noisy 11 | stock exchange, or your kitchen in the morning--everything confused and 12 | jumbled. When you think of the word _order_, perhaps you think of an empty room, 13 | serene and calm. For scientists, though, chaos is characterized by homogeneity 14 | (sameness), and order by complexity (difference). 15 | 16 | //// 17 | IDEA [SG] Found previous paragraph a bit confusing. It seems to suggest that a 18 | scientist would say that a noisy stock exchange is ordered. I feel like you 19 | want to talk about Entropy but do not want to go down that rabbit hole. 20 | //// 21 | 22 | For example, a well-tended garden is a highly ordered system. Gardeners define 23 | boundaries with paths and fences, and they mark out flower beds or vegetable 24 | patches. Over time, the garden evolves, growing richer and thicker; but without 25 | deliberate effort, the garden will run wild. Weeds and grasses will choke out 26 | other plants, covering over the paths, until eventually every part looks the 27 | same again--wild and unmanaged. 28 | 29 | Software systems, too, tend toward chaos. When we first start building a new 30 | system, we have grand ideas that our code will be clean and well ordered, but 31 | over time we find that it gathers cruft and edge cases and ends up a confusing 32 | morass of manager classes and util modules. We find that our sensibly layered 33 | architecture has collapsed into itself like an oversoggy trifle. Chaotic 34 | software systems are characterized by a sameness of function: API handlers that 35 | have domain knowledge and send email and perform logging; "business logic" 36 | classes that perform no calculations but do perform I/O; and everything coupled 37 | to everything else so that changing any part of the system becomes fraught with 38 | danger. This is so common that software engineers have their own term for 39 | chaos: the Big Ball of Mud antipattern (<<bbom_image>>). 40 | 41 | [[bbom_image]] 42 | .A real-life dependency diagram (source: https://oreil.ly/dbGTW["Enterprise Dependency: Big Ball of Yarn"] by Alex Papadimoulis) 43 | image::images/apwp_0001.png[] 44 | 45 | TIP: A big ball of mud is the natural state of software in the same way that wilderness 46 | is the natural state of your garden. It takes energy and direction to 47 | prevent the collapse. 48 | 49 | Fortunately, the techniques to avoid creating a big ball of mud aren't complex. 50 | 51 | // IDEA: talk about how architecture enables TDD and DDD (ie callback to book 52 | // subtitle) 53 | 54 | === Encapsulation and Abstractions 55 | 56 | Encapsulation and abstraction are tools that we all instinctively reach for 57 | as programmers, even if we don't all use these exact words. Allow us to dwell 58 | on them for a moment, since they are a recurring background theme of the book. 59 | 60 | The term _encapsulation_ covers two closely related ideas: simplifying 61 | behavior and hiding data. In this discussion, we're using the first sense. We 62 | encapsulate behavior by identifying a task that needs to be done in our code 63 | and giving that task to a well-defined object or function. We call that object or function an 64 | _abstraction_. 65 | 66 | //DS: not sure I agree with this definition. more about establishing boundaries? 67 | 68 | Take a look at the following two snippets of Python code: 69 | 70 | 71 | [[urllib_example]] 72 | .Do a search with urllib 73 | ==== 74 | [source,python] 75 | ---- 76 | import json 77 | from urllib.request import urlopen 78 | from urllib.parse import urlencode 79 | 80 | params = dict(q='Sausages', format='json') 81 | handle = urlopen('http://api.duckduckgo.com' + '?' + urlencode(params)) 82 | raw_text = handle.read().decode('utf8') 83 | parsed = json.loads(raw_text) 84 | 85 | results = parsed['RelatedTopics'] 86 | for r in results: 87 | if 'Text' in r: 88 | print(r['FirstURL'] + ' - ' + r['Text']) 89 | ---- 90 | ==== 91 | 92 | [[requests_example]] 93 | .Do a search with requests 94 | ==== 95 | [source,python] 96 | ---- 97 | import requests 98 | 99 | params = dict(q='Sausages', format='json') 100 | parsed = requests.get('http://api.duckduckgo.com/', params=params).json() 101 | 102 | results = parsed['RelatedTopics'] 103 | for r in results: 104 | if 'Text' in r: 105 | print(r['FirstURL'] + ' - ' + r['Text']) 106 | ---- 107 | ==== 108 | 109 | Both code listings do the same thing: they submit form-encoded values 110 | to a URL in order to use a search engine API. But the second is simpler to read 111 | and understand because it operates at a higher level of abstraction. 112 | 113 | We can take this one step further still by identifying and naming the task we 114 | want the code to perform for us and using an even higher-level abstraction to make 115 | it explicit: 116 | 117 | [[ddg_example]] 118 | .Do a search with the duckduckgo client library 119 | ==== 120 | [source,python] 121 | ---- 122 | import duckduckpy 123 | for r in duckduckpy.query('Sausages').related_topics: 124 | print(r.first_url, ' - ', r.text) 125 | ---- 126 | ==== 127 | 128 | Encapsulating behavior by using abstractions is a powerful tool for making 129 | code more expressive, more testable, and easier to maintain. 130 | 131 | NOTE: In the literature of the object-oriented (OO) world, one of the classic 132 | characterizations of this approach is called 133 | http://www.wirfs-brock.com/Design.html[_responsibility-driven design_]; 134 | it uses the words _roles_ and _responsibilities_ rather than _tasks_. 135 | The main point is to think about code in terms of behavior, rather than 136 | in terms of data or algorithms.footnote:[If you've come across 137 | class-responsibility-collaborator (CRC) cards, they're 138 | driving at the same thing: thinking about _responsibilities_ helps you decide how to split things up.] 139 | 140 | .Abstractions and ABCs 141 | ******************************************************************************* 142 | In a traditional OO language like Java or C#, you might use an abstract base 143 | class (ABC) or an interface to define an abstraction. In Python you can (and we 144 | sometimes do) use ABCs, but you can also happily rely on duck typing. 145 | 146 | The abstraction can just mean "the public API of the thing you're using"—a 147 | function name plus some arguments, for example. 148 | ******************************************************************************* 149 | 150 | Most of the patterns in this book involve choosing an abstraction, so you'll 151 | see plenty of examples in each chapter. In addition, 152 | <<chapter_03_abstractions>> specifically discusses some general heuristics 153 | for choosing abstractions. 154 | 155 | 156 | === Layering 157 | 158 | Encapsulation and abstraction help us by hiding details and protecting the 159 | consistency of our data, but we also need to pay attention to the interactions 160 | between our objects and functions. When one function, module, or object uses 161 | another, we say that the one _depends on_ the other. These dependencies form a 162 | kind of network or graph. 163 | 164 | In a big ball of mud, the dependencies are out of control (as you saw in 165 | <<bbom_image>>). Changing one node of the graph becomes difficult because it 166 | has the potential to affect many other parts of the system. Layered 167 | architectures are one way of tackling this problem. In a layered architecture, 168 | we divide our code into discrete categories or roles, and we introduce rules 169 | about which categories of code can call each other. 170 | 171 | One of the most common examples is the _three-layered architecture_ shown in 172 | <<layered_architecture1>>. 173 | 174 | [role="width-75"] 175 | [[layered_architecture1]] 176 | .Layered architecture 177 | image::images/apwp_0002.png[] 178 | [role="image-source"] 179 | ---- 180 | [ditaa, apwp_0002] 181 | +----------------------------------------------------+ 182 | | Presentation Layer | 183 | +----------------------------------------------------+ 184 | | 185 | V 186 | +----------------------------------------------------+ 187 | | Business Logic | 188 | +----------------------------------------------------+ 189 | | 190 | V 191 | +----------------------------------------------------+ 192 | | Database Layer | 193 | +----------------------------------------------------+ 194 | ---- 195 | 196 | 197 | Layered architecture is perhaps the most common pattern for building business 198 | software. In this model we have user-interface components, which could be a web 199 | page, an API, or a command line; these user-interface components communicate 200 | with a business logic layer that contains our business rules and our workflows; 201 | and finally, we have a database layer that's responsible for storing and retrieving 202 | data. 203 | 204 | For the rest of this book, we're going to be systematically turning this 205 | model inside out by obeying one simple principle. 206 | 207 | 208 | [[dip]] 209 | === The Dependency Inversion Principle 210 | 211 | You might be familiar with the _dependency inversion principle_ (DIP) already, because 212 | it's the _D_ in SOLID.footnote:[SOLID is an acronym for Robert C. Martin's five principles of object-oriented 213 | design: single responsibility, open for extension but 214 | closed for modification, Liskov substitution, interface segregation, and 215 | dependency inversion. See https://oreil.ly/UFM7U["S.O.L.I.D: The First 5 Principles of Object-Oriented Design"] by Samuel Oloruntoba.] 216 | 217 | Unfortunately, we can't illustrate the DIP by using three tiny code listings as 218 | we did for encapsulation. However, the whole of <<part1>> is essentially a worked 219 | example of implementing the DIP throughout an application, so you'll get 220 | your fill of concrete examples. 221 | 222 | In the meantime, we can talk about DIP's formal definition: 223 | 224 | // [SG] reference? 225 | 226 | 1. High-level modules should not depend on low-level modules. Both should 227 | depend on abstractions. 228 | 229 | 2. Abstractions should not depend on details. Instead, details should depend on 230 | abstractions. 231 | 232 | But what does this mean? Let's take it bit by bit. 233 | 234 | _High-level modules_ are the code that your organization really cares about. 235 | Perhaps you work for a pharmaceutical company, and your high-level modules deal 236 | with patients and trials. Perhaps you work for a bank, and your high-level 237 | modules manage trades and exchanges. The high-level modules of a software 238 | system are the functions, classes, and packages that deal with our real-world 239 | concepts. 240 | 241 | By contrast, _low-level modules_ are the code that your organization doesn't 242 | care about. It's unlikely that your HR department gets excited about filesystems or network sockets. It's not often that you discuss SMTP, HTTP, 243 | or AMQP with your finance team. For our nontechnical stakeholders, these 244 | low-level concepts aren't interesting or relevant. All they care about is 245 | whether the high-level concepts work correctly. If payroll runs on time, your 246 | business is unlikely to care whether that's a cron job or a transient function 247 | running on Kubernetes. 248 | 249 | _Depends on_ doesn't mean _imports_ or _calls_, necessarily, but rather a more 250 | general idea that one module _knows about_ or _needs_ another module. 251 | 252 | And we've mentioned _abstractions_ already: they're simplified interfaces that 253 | encapsulate behavior, in the way that our duckduckgo module encapsulated a 254 | search engine's API. 255 | 256 | [quote,David Wheeler] 257 | ____ 258 | All problems in computer science can be solved by adding another level of 259 | indirection. 260 | ____ 261 | 262 | So the first part of the DIP says that our business code shouldn't depend on 263 | technical details; instead, both should use abstractions. 264 | 265 | Why? Broadly, because we want to be able to change them independently of each 266 | other. High-level modules should be easy to change in response to business 267 | needs. Low-level modules (details) are often, in practice, harder to 268 | change: think about refactoring to change a function name versus defining, testing, 269 | and deploying a database migration to change a column name. We don't 270 | want business logic changes to slow down because they are closely coupled 271 | to low-level infrastructure details. But, similarly, it is important to _be 272 | able_ to change your infrastructure details when you need to (think about 273 | sharding a database, for example), without needing to make changes to your 274 | business layer. Adding an abstraction between them (the famous extra 275 | layer of indirection) allows the two to change (more) independently of each 276 | other. 277 | 278 | The second part is even more mysterious. "Abstractions should not depend on 279 | details" seems clear enough, but "Details should depend on abstractions" is 280 | hard to imagine. How can we have an abstraction that doesn't depend on the 281 | details it's abstracting? By the time we get to <<chapter_04_service_layer>>, 282 | we'll have a concrete example that should make this all a bit clearer. 283 | 284 | 285 | === A Place for All Our Business Logic: The Domain Model 286 | 287 | But before we can turn our three-layered architecture inside out, we need to 288 | talk more about that middle layer: the high-level modules or business 289 | logic. One of the most common reasons that our designs go wrong is that 290 | business logic becomes spread throughout the layers of our application, 291 | making it hard to identify, understand, and change. 292 | 293 | <<chapter_01_domain_model>> shows how to build a business 294 | layer with a _Domain Model_ pattern. The rest of the patterns in <<part1>> show 295 | how we can keep the domain model easy to change and free of low-level concerns 296 | by choosing the right abstractions and continuously applying the DIP. 297 | -------------------------------------------------------------------------------- /ix.html: -------------------------------------------------------------------------------- 1 | <!-- This is a placeholder element for use with the automatic index generation option in Atlas --> 2 | <section data-type="index"/> 3 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Creative Commons CC-By-ND 2 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | ignore_missing_imports = False 3 | namespace_packages = True 4 | mypy_path = ./code/src 5 | check_untyped_defs = True 6 | 7 | [mypy-pytest.*,lxml.*,sqlalchemy.*,redis.*,django.*] 8 | ignore_missing_imports = True 9 | -------------------------------------------------------------------------------- /outline.md: -------------------------------------------------------------------------------- 1 | # Outline 2 | 3 | ## Preface/Intro 4 | 5 | Who are we, why we are writing this book 6 | (all the args from the proposal, python's popularity, communicating well-understood patterns from the Java/C#/Enterprise world to a wider audience with nice, readable, pythonic code examples) 7 | 8 | > As Python grows in popularity as a language, typical projects are getting larger and more complex, and issues of software design and architecture become more salient. Patterns like "Ports and Adapters", as well Domain-Driven Design, Event-Driven programming, Command-Query Responsibility Segregation, which are well known in more "enterprisey" communities like Java and C#, are generating more interest in the Python community, but it's not always easy to see how these patterns translate into Python. (reviewing the classic "Gang of Four" design patterns book for example leads to the conclusion that half the patterns are artifacts of the Java/C++ syntax, and are simply not necessary in the more flexible and dynamic world of Python). 9 | 10 | > In the Python world, we often quote the Zen of Python: "there should be one--preferably on only one--obvious way to do it". Unfortunately, as project complexity grows, the most obvious way of doing things isn't always the way that helps you manage complexity and evolving requirements. 11 | 12 | > This book will provide an introduction to proven architectural design patterns that help you manage complexity, using concrete examples written in Python. It will explain how to avoid some of the unnecessary particularities of Java and C# syntax and implement these patterns in a "Pythonic" way. 13 | 14 | An overview of the central argument: 15 | 16 | > Layer your system so that the low-level details depend on the high-level abstractions, not the other way around. 17 | 18 | 19 | ## Chapter 1: Domain modelling, and why do we always make it so hard for ourselves? 20 | 21 | Every so often someone says "where shall I put this new logic", and we all know the right answer: it should live in the domain model. So why does it always somehow end up in some gigantic controller function that's closely coupled with your web framework and database and third party apis and god knows what else? 22 | 23 | Let's see what happens if we build everything around making sure our domain model is easy to work with and change. 24 | 25 | * Talk about domain modelling and DDD, work through an example (allocate-order) 26 | * Example code 27 | * crazy idea: Bob and I will record a video of (part of?) the allocate-order code writing as a TDD "kata", to illustrate how easy it is to do this stuff when you have no dependencies on databases, frameworks, etc (this will also save us from having to go slowly thru what tdd is in the book) 28 | 29 | code examples / patterns: some domain model objects (python3.7 dataclasses maybe?), a domain service / use case function, some "proper" unit tests. 30 | 31 | related post from existing blog: https://io.made.com/introducing-command-handler/ 32 | 33 | 34 | 35 | ## Chapter 2: persistence and the repository pattern 36 | 37 | The main ORM tool in the Python world is SQLAlchemy, and if you follow the default tutorial, you end up writing your model objects inheriting from `sqlalchemy.Table`, and soon your domain model is tightly coupled to your DB. 38 | 39 | But you don't have to! Demonstrate the alternative way to do metadata/mapping. 40 | 41 | ==> our ORM depends on the domain model, and not the other way around. an illustration of one of our key patterns/principles, the _Dependency Inversion Principle_ (the D in SOLID) 42 | 43 | Also: repository pattern. choosing a simple abstraction (it's a dictionary) 44 | Also: first integration test 45 | 46 | code examples / patterns: sqlalchemy metadata/mapping, repository pattern 47 | 48 | related post from existing blog: https://io.made.com/repository-and-unit-of-work-pattern-in-python/ 49 | 50 | 51 | 52 | ## Chapter 3: making ourselves available via a web API. Flask as a port (as in ports-and-adapters). Our first use case. Orchestration. Service layer 53 | 54 | We have a use case in mind, and a domain model that can do it, but how do we make it available to the outside world? 55 | 56 | start with a naive flask controller. evolve it to flask being an adapter to a use case function in our service/orchestration layer. 57 | 58 | * happy path, show the basic use case moving parts: create a database session, initialise our repository, load some objects, invoke our domain function, commit. 59 | * first acceptance test 60 | * handle error case, eg product does not exist. handle `KeyError` from repository, flask returns a 400 with nice erro json 61 | * but what if we have more error cases at this orchestration level? it'll be annoying to test everything with acceptance tests, and hard to unit test. 62 | 63 | ==> introduce service layer. flask becomes an adapter. flask depends on the service layer, rather than the other way around (DIP once again) 64 | 65 | (later in the book we'll swap flask for asyncio, and show how easy it is) 66 | 67 | patterns: use case, service layer, port/adapter pattern for web, 68 | 69 | related post from existing blog: https://io.made.com/repository-and-unit-of-work-pattern-in-python/ 70 | 71 | 72 | 73 | ## Chapter 4: data integrity concerns 1: unit of work pattern 74 | 75 | What happens if we encounter an error during our allocation? eg out of stock, a domain error? We'd like to wrap our work up so that, either the entire order is allocated, or we abort and leave things in a "clean" state if anything goes wrong -- a classic case for a transaction/rollback. 76 | 77 | What's a Pythonic way of "doing transactions"? A context manager. demonstrate the _Unit of Work Pattern_ and show how it fits with _Repository_ 78 | But we also want a nice, Pythonic way of "doing transactions", of wrapping a block of code up into an atomic whole. 79 | 80 | discuss different options of unit of work, explicit/implicit rollbacks, dependency-inject uow. 81 | 82 | code examples / patterns: Unit Of Work (using a context manager) 83 | 84 | related post from existing blog: https://io.made.com/repository-and-unit-of-work-pattern-in-python/ 85 | 86 | 87 | ## Chapter 5: data integrity concerns 2: choosing the right consistency boundary (Aggregate pattern) 88 | 89 | While we're thinking about data integrity, we now realise that our `allocate(order, shipments)` implementation which depends on all the shipments in the world won't scale if every order needs to lock the whole shipments table. We should only look at shipments that matter to that order. 90 | 91 | Moreover, we only need to allocate the order one line at a time (although maybe we want to roll back all the lines if we fail any one of them). 92 | 93 | This leads us on to discussing the _Aggregate_ pattern - by choosing `Product` as our aggregate, we choose a consistency boundary that allows us to be more clever about transactions. 94 | 95 | Also demonstrate how easy it is to refactor a domain model if it's not intermixed with infrastructure concerns. 96 | 97 | code examples / patterns: Aggregate 98 | 99 | 100 | 101 | ## Chapter 6: event-driven architecture part 1: doamin events and the message bus 102 | 103 | Another new requirement: when allocation fails because we are out of stock, someone should be emailed. But we don't want to have email-sending code be a potential cause of bugs/failures in our core model/state changes. introduce domain events and a message bus as a pattern for kicking off related work after a use case is complete. 104 | 105 | * discuss SRP, use case shouldn't have an _and_. leads naturally to events. 106 | 107 | code examples / patterns: events, handlers, message bus 108 | 109 | related post from existing blog: https://io.made.com/why-use-domain-events/ 110 | 111 | 112 | 113 | ## Chapter 7: event-driven architecture part 2: reactive microservices 114 | 115 | We've got a microservice with an web api, but what about other ways of talking to other systems? how does it know if, say, a 116 | shipment is delayed or the quantity is amended? how does it communicate to our warehouse system to say that an order has 117 | been allocated and needs to be sent to a customer? 118 | 119 | * redis pubsub in: batch_quantity_changed. leads to deallocate / reallocate 120 | * redis pubsub out: order_allocated event, for communicating with warehouse 121 | 122 | code examples / patterns: events as a microservices integration platform 123 | 124 | 125 | ## Chapter 8: Commands vs Events 126 | 127 | distinguish commands from events. different semantics, can say no to a command. 128 | 129 | 130 | 131 | ## Chapter 9: CQRS 132 | 133 | The business comes along and supplies a new requirement: a dashboard showing the current allocation state of all shipments. Discuss how using the ORM naively leads to the _SELECT N+1_ antipattern, and use it as an opportunity to demonstrate _Command-Query-Responsiblity-Segregation (CQRS)_ -- read-only parts of our architecture can be implemented quite differently from the write side 134 | 135 | start with raw sql 136 | discuss option of using events to update a view model 137 | 138 | code examples / patterns: CQRS / event-driven view model. 139 | 140 | related post from existing blog: https://io.made.com/commands-and-queries-handlers-and-views/ 141 | 142 | 143 | ## Chapter 10: Bootstrap.py and dependency injection 144 | 145 | the database / sqlalchemy are under control, but email and redis are a bit haphazard. also orm initialisation. show how a bootsrap script 146 | and DI can manage all this 147 | 148 | diagrams. 149 | 150 | with + without framework, `@inject`, and bob's crazy, heretical, unclean type-hints based one. 151 | 152 | maybe point out that command-handler pattern would make this all easier? 153 | 154 | related post from existing blog: https://io.made.com/dependency-injection-with-type-signatures-in-python/ 155 | 156 | 157 | ## Appendix 1: project structure 158 | 159 | show folder structure, dockerfiles, setup.py, where tests live, etc. 160 | 161 | 162 | 163 | ## Appendix 2: swapping out flask and our database for a CLI with CSVs 164 | 165 | (follows on from unit of work chapter). 166 | the business come to us apologetically saying they're not ready to use our API and could we build a thing that reads batches and orders from 2 csvs and outputs a third with allocations". 167 | 168 | show how by just changing our repository and unitofwork, we can use the exact same service layer and domain layer to build a CLI app. 169 | 170 | this could be an exercise for the reader tbh. or a video 171 | 172 | 173 | ## appendix 3: command handler pattern? 174 | 175 | ==> show how commands can be put on the message bus just like events. 176 | 177 | code examples / patterns: reuse message bus for commands 178 | 179 | related post from existing blog: https://io.made.com/introducing-command-handler/ 180 | 181 | 182 | 183 | -------------------------------------------------------------------------------- /part1.asciidoc: -------------------------------------------------------------------------------- 1 | [role="pagenumrestart"] 2 | [[part1]] 3 | [part] 4 | == Building an Architecture to Support Domain Modeling 5 | 6 | 7 | [quote, Cyrille Martraire, DDD EU 2017] 8 | ____ 9 | Most developers have never seen a domain model, only a data model. 10 | ____ 11 | 12 | Most developers we talk to about architecture have a nagging sense that 13 | things could be better. They are often trying to rescue a system that has gone 14 | wrong somehow, and are trying to put some structure back into a ball of mud. 15 | They know that their business logic shouldn't be spread all over the place, 16 | but they have no idea how to fix it. 17 | 18 | We've found that many developers, when asked to design a new system, will 19 | immediately start to build a database schema, with the object model treated 20 | as an afterthought. This is where it all starts to go wrong. Instead, _behavior 21 | should come first and drive our storage requirements._ After all, our customers don't care about the data model. They care about what 22 | the system _does_; otherwise they'd just use a spreadsheet. 23 | 24 | The first part of the book looks at how to build a rich object model 25 | through TDD (in <<chapter_01_domain_model>>), and then we'll show how 26 | to keep that model decoupled from technical concerns. We show how to build 27 | persistence-ignorant code and how to create stable APIs around our domain so 28 | that we can refactor aggressively. 29 | 30 | To do that, we present four key design patterns: 31 | 32 | * The <<chapter_02_repository,Repository pattern>>, an abstraction over the 33 | idea of persistent storage 34 | 35 | * The <<chapter_04_service_layer,Service Layer pattern>> to clearly define where our 36 | use cases begin and end 37 | 38 | [role="pagebreak-before"] 39 | * The <<chapter_06_uow,Unit of Work pattern>> to provide atomic operations 40 | 41 | * The <<chapter_07_aggregate,Aggregate pattern>> to enforce the integrity 42 | of our data 43 | 44 | If you'd like a picture of where we're going, take a look at 45 | <<part1_components_diagram>>, but don't worry if none of it makes sense 46 | yet! We introduce each box in the figure, one by one, throughout this part of the book. 47 | 48 | [role="width-90"] 49 | [[part1_components_diagram]] 50 | .A component diagram for our app at the end of <<part1>> 51 | image::images/apwp_p101.png[] 52 | 53 | We also take a little time out to talk about 54 | <<chapter_03_abstractions,coupling and abstractions>>, illustrating it with a simple example that shows how and why we choose our 55 | abstractions. 56 | 57 | Three appendices are further explorations of the content from Part I: 58 | 59 | * <<appendix_project_structure>> is a write-up of the infrastructure for our example 60 | code: how we build and run the Docker images, where we manage configuration 61 | info, and how we run different types of tests. 62 | 63 | * <<appendix_csvs>> is a "proof of the pudding" kind of content, showing 64 | how easy it is to swap out our entire infrastructure--the Flask API, the 65 | ORM, and Postgres—for a totally different I/O model involving a CLI and 66 | CSVs. 67 | 68 | * Finally, <<appendix_django>> may be of interest if you're wondering how these 69 | patterns might look if using Django instead of Flask and SQLAlchemy. 70 | -------------------------------------------------------------------------------- /part2.asciidoc: -------------------------------------------------------------------------------- 1 | [[part2]] 2 | [part] 3 | == Event-Driven Architecture 4 | 5 | [quote, Alan Kay] 6 | ____ 7 | 8 | I'm sorry that I long ago coined the term "objects" for this topic because it 9 | gets many people to focus on the lesser idea. 10 | 11 | The big idea is "messaging."...The key in making great and growable systems is 12 | much more to design how its modules communicate rather than what their internal 13 | properties and behaviors should be. 14 | ____ 15 | 16 | It's all very well being able to write _one_ domain model to manage a single bit 17 | of business process, but what happens when we need to write _many_ models? In 18 | the real world, our applications sit within an organization and need to exchange 19 | information with other parts of the system. You may remember our context 20 | diagram shown in <<allocation_context_diagram_again>>. 21 | 22 | Faced with this requirement, many teams reach for microservices integrated 23 | via HTTP APIs. But if they're not careful, they'll end up producing the most 24 | chaotic mess of all: the distributed big ball of mud. 25 | 26 | In Part II, we'll show how the techniques from <<part1>> can be extended to 27 | distributed systems. We'll zoom out to look at how we can compose a system from 28 | many small components that interact through asynchronous message passing. 29 | 30 | We'll see how our Service Layer and Unit of Work patterns allow us to reconfigure our app 31 | to run as an asynchronous message processor, and how event-driven systems help 32 | us to decouple aggregates and applications from one another. 33 | 34 | [[allocation_context_diagram_again]] 35 | .But exactly how will all these systems talk to each other? 36 | image::images/apwp_0102.png[] 37 | 38 | 39 | // TODO: DS - this might give the impression that the whole of part 2 40 | // is irrelevant for readers in a monolith context 41 | 42 | //IDEA (DS): It seems to me the two key themes in this book are vertical and 43 | //horizontal decoupling. Did you consider choosing those for the two parts? 44 | 45 | We'll look at the following patterns and techniques: 46 | 47 | Domain Events:: 48 | Trigger workflows that cross consistency boundaries. 49 | 50 | Message Bus:: 51 | Provide a unified way of invoking use cases from any endpoint. 52 | 53 | CQRS:: 54 | Separating reads and writes avoids awkward compromises in an event-driven 55 | architecture and enables performance and scalability improvements. 56 | 57 | Plus, we'll add a dependency injection framework. This has nothing to do with 58 | event-driven architecture per se, but it tidies up an awful lot of loose 59 | ends. 60 | 61 | // IDEA: a bit of blurb about making events more central to our design thinking? 62 | -------------------------------------------------------------------------------- /plantuml.cfg: -------------------------------------------------------------------------------- 1 | skinparam default { 2 | FontName Guardian Sans Cond Regular 3 | FontSize 18 4 | FontColor Black 5 | } 6 | 7 | skinparam class { 8 | BackgroundColor #b5e2fa 9 | BorderColor #0fa3b1 10 | } 11 | 12 | skinparam CircledCharacter { 13 | FontColor Black 14 | } 15 | 16 | skinparam stereotypeC { 17 | BackgroundColor #eddea4 18 | } 19 | 20 | skinparam package { 21 | FontName Guardian Sans Cond Light 22 | } 23 | 24 | skinparam sequencelifeline { 25 | BorderColor #0fa3b1 26 | } 27 | skinparam arrow { 28 | Color #0fa3b1 29 | } 30 | skinparam participant { 31 | BackgroundColor #b5e2fa 32 | BorderColor #0fa3b1 33 | } 34 | skinparam entity { 35 | BackgroundColor #b5e2fa 36 | BorderColor #0fa3b1 37 | } 38 | skinparam collections { 39 | BackgroundColor #b5e2fa 40 | BorderColor #0fa3b1 41 | } 42 | skinparam database { 43 | BackgroundColor #b5e2fa 44 | BorderColor #0fa3b1 45 | } 46 | skinparam boundary { 47 | BackgroundColor #b5e2fa 48 | BorderColor #0fa3b1 49 | } 50 | skinparam actor { 51 | Color DeepSkyBlue 52 | BackgroundColor #b5e2fa 53 | BorderColor #0fa3b1 54 | } 55 | skinparam sequencegroupheader { 56 | FontName Guardian Sans Cond Light 57 | } 58 | skinparam sequencebox { 59 | BackgroundColor PowderBlue 60 | BorderColor #0fa3b1 61 | } 62 | skinparam padding 4 63 | -------------------------------------------------------------------------------- /preface.asciidoc: -------------------------------------------------------------------------------- 1 | [[preface]] 2 | [preface] 3 | == Preface 4 | 5 | You may be wondering who we are and why we wrote this book. 6 | 7 | At the end of Harry's last book, 8 | http://www.obeythetestinggoat.com[_Test-Driven Development with Python_] (O'Reilly), 9 | he found himself asking a bunch of questions about architecture, such as, 10 | What's the best way of structuring your application so that it's easy to test? 11 | More specifically, so that your core business logic is covered by unit tests, 12 | and so that you minimize the number of integration and end-to-end tests you need? 13 | He made vague references to "Hexagonal Architecture" and "Ports and Adapters" 14 | and "Functional Core, Imperative Shell," but if he was honest, he'd have to 15 | admit that these weren't things he really understood or had done in practice. 16 | 17 | And then he was lucky enough to run into Bob, who has the answers to all these 18 | questions. 19 | 20 | Bob ended up as a software architect because nobody else on his team was 21 | doing it. He turned out to be pretty bad at it, but _he_ was lucky enough to run 22 | into Ian Cooper, who taught him new ways of writing and thinking about code. 23 | 24 | === Managing Complexity, Solving Business Problems 25 | 26 | We both work for MADE.com, a European ecommerce company that sells furniture 27 | online; there, we apply the techniques in this book to build distributed systems 28 | that model real-world business problems. Our example domain is the first system 29 | Bob built for MADE, and this book is an attempt to write down all the _stuff_ we 30 | have to teach new programmers when they join one of our teams. 31 | 32 | MADE.com operates a global supply chain of freight partners and manufacturers. 33 | To keep costs low, we try to optimize the delivery of stock to our 34 | warehouses so that we don't have unsold goods lying around the place. 35 | 36 | Ideally, the sofa that you want to buy will arrive in port on the very day 37 | that you decide to buy it, and we'll ship it straight to your house without 38 | ever storing it. [.keep-together]#Getting# the timing right is a tricky balancing act when goods take 39 | three months to arrive by container ship. Along the way, things get broken or water 40 | damaged, storms cause unexpected delays, logistics partners mishandle goods, 41 | paperwork goes missing, customers change their minds and amend their orders, 42 | and so on. 43 | 44 | We solve those problems by building intelligent software representing the 45 | kinds of operations taking place in the real world so that we can automate as 46 | much of the business as possible. 47 | 48 | === Why Python? 49 | 50 | If you're reading this book, we probably don't need to convince you that Python 51 | is great, so the real question is "Why does the _Python_ community need a book 52 | like this?" The answer is about Python's popularity and maturity: although Python is 53 | probably the world's fastest-growing programming language and is nearing the top 54 | of the absolute popularity tables, it's only just starting to take on the kinds 55 | of problems that the C# and Java world has been working on for years. 56 | Startups become real businesses; web apps and scripted automations are becoming 57 | (whisper it) _enterprise_ [.keep-together]#_software_#. 58 | 59 | In the Python world, we often quote the Zen of Python: 60 | "There should be one--and preferably only one--obvious way to do it."footnote:[`python -c "import this"`] 61 | Unfortunately, as project size grows, the most obvious way of doing things 62 | isn't always the way that helps you manage complexity and evolving 63 | requirements. 64 | 65 | None of the techniques and patterns we discuss in this book are 66 | new, but they are mostly new to the Python world. And this book isn't 67 | a replacement for the classics in the field such as Eric Evans's 68 | _Domain-Driven Design_ 69 | or Martin Fowler's _Patterns of 70 | Enterprise Application Architecture_ (both published by Addison-Wesley [.keep-together]#Professional#)—which we often refer to and 71 | encourage you to go and read. 72 | 73 | But all the classic code examples in the literature do tend to be written in 74 | Java or pass:[<span class="keep-together">C++/#</span>], and if you're a Python person and haven't used either of 75 | those languages in a long time (or indeed ever), those code listings can be 76 | quite...trying. There's a reason the latest edition of that other classic text, Fowler's 77 | _Refactoring_ (Addison-Wesley Professional), is in JavaScript. 78 | 79 | [role="pagebreak-before less_space"] 80 | === TDD, DDD, and Event-Driven Architecture 81 | 82 | In order of notoriety, we know of three tools for managing complexity: 83 | 84 | 1. _Test-driven development_ (TDD) helps us to build code that is correct 85 | and enables us to refactor or add new features, without fear of regression. 86 | But it can be hard to get the best out of our tests: How do we make sure 87 | that they run as fast as possible? That we get as much coverage and feedback 88 | from fast, dependency-free unit tests and have the minimum number of slower, 89 | flaky end-to-end tests? 90 | 91 | 2. _Domain-driven design_ (DDD) asks us to focus our efforts on building a good 92 | model of the business domain, but how do we make sure that our models aren't 93 | encumbered with infrastructure concerns and don't become hard to change? 94 | 95 | 3. Loosely coupled (micro)services integrated via messages (sometimes called 96 | _reactive microservices_) are a well-established answer to managing complexity 97 | across multiple applications or business domains. But it's not always 98 | obvious how to make them fit with the established tools of 99 | the Python world--Flask, Django, Celery, and so on. 100 | 101 | NOTE: Don't be put off if you're not working with (or interested in) microservices. 102 | The vast majority of the patterns we discuss, 103 | including much of the event-driven architecture material, 104 | is absolutely applicable in a monolithic architecture. 105 | 106 | Our aim with this book is to introduce several classic architectural patterns 107 | and show how they support TDD, DDD, and event-driven services. We hope 108 | it will serve as a reference for implementing them in a Pythonic way, and that 109 | people can use it as a first step toward further research in this field. 110 | 111 | 112 | === Who Should Read This Book 113 | 114 | Here are a few things we assume about you, dear reader: 115 | 116 | * You've been close to some reasonably complex Python applications. 117 | 118 | * You've seen some of the pain that comes with trying to manage 119 | that complexity. 120 | 121 | * You don't necessarily know anything about DDD or any of the 122 | classic application architecture patterns. 123 | 124 | We structure our explorations of architectural patterns around an example app, 125 | building it up chapter by chapter. We use TDD at 126 | work, so we tend to show listings of tests first, followed by implementation. 127 | If you're not used to working test-first, it may feel a little strange at 128 | the beginning, but we hope you'll soon get used to seeing code "being used" 129 | (i.e., from the outside) before you see how it's built on the inside. 130 | 131 | We use some specific Python frameworks and technologies, including Flask, 132 | SQLAlchemy, and pytest, as well as Docker and Redis. If you're already 133 | familiar with them, that won't hurt, but we don't think it's required. One of 134 | our main aims with this book is to build an architecture for which specific 135 | technology choices become minor implementation details. 136 | 137 | === A Brief Overview of What You'll Learn 138 | 139 | The book is divided into two parts; here's a look at the topics we'll cover 140 | and the chapters they live in. 141 | 142 | ==== pass:[<a data-type="xref" data-xrefstyle="chap-num-title" href="#part1">#part1</a>] 143 | 144 | Domain modeling and DDD (Chapters <<chapter_01_domain_model,1>>, <<chapter_02_repository,2>> and <<chapter_07_aggregate,7>>):: 145 | At some level, everyone has learned the lesson that complex business 146 | problems need to be reflected in code, in the form of a model of the domain. 147 | But why does it always seem to be so hard to do without getting tangled 148 | up with infrastructure concerns, our web frameworks, or whatever else? 149 | In the first chapter we give a broad overview of _domain modeling_ and DDD, and we 150 | show how to get started with a model that has no external dependencies, and 151 | fast unit tests. Later we return to DDD patterns to discuss how to choose 152 | the right aggregate, and how this choice relates to questions of data 153 | integrity. 154 | 155 | Repository, Service Layer, and Unit of Work patterns (Chapters <<chapter_02_repository,2>>, <<chapter_04_service_layer,4>>, and <<chapter_05_high_gear_low_gear,5>>):: 156 | In these three chapters we present three closely related and 157 | mutually reinforcing patterns that support our ambition to keep 158 | the model free of extraneous dependencies. We build a layer of 159 | abstraction around persistent storage, and we build a service 160 | layer to define the entrypoints to our system and capture the 161 | primary use cases. We show how this layer makes it easy to build 162 | thin entrypoints to our system, whether it's a Flask API or a CLI. 163 | 164 | // [SG] Bit of pedantry - this is the first time you have used CLI acronym, 165 | // should be spelled out? 166 | 167 | Some thoughts on testing and abstractions (Chapter <<chapter_03_abstractions,3>> and <<chapter_05_high_gear_low_gear,5>>):: 168 | After presenting the first abstraction (the Repository pattern), we take the 169 | opportunity for a general discussion of how to choose abstractions, and 170 | what their role is in choosing how our software is coupled together. After 171 | we introduce the Service Layer pattern, we talk a bit about achieving a _test pyramid_ 172 | and writing unit tests at the highest possible level of abstraction. 173 | 174 | 175 | 176 | ==== pass:[<a data-type="xref" data-xrefstyle="chap-num-title" href="#part2">#part2</a>] 177 | 178 | Event-driven architecture (Chapters <<chapter_08_events_and_message_bus,8>>-<<chapter_11_external_events,11>>):: 179 | We introduce three more mutually reinforcing patterns: 180 | the Domain Events, Message Bus, and Handler patterns. 181 | _Domain events_ are a vehicle for capturing the idea that 182 | some interactions with a system are triggers for others. 183 | We use a _message bus_ to allow actions to trigger events 184 | and call appropriate _handlers_. 185 | We move on to discuss how events can be used as a pattern 186 | for integration between services in a microservices architecture. 187 | Finally, we distinguish between _commands_ and _events_. 188 | Our application is now fundamentally a message-processing system. 189 | 190 | Command-query responsibility segregation (<<chapter_12_cqrs>>):: 191 | We present an example of _command-query responsibility segregation_, 192 | with and without events. 193 | 194 | Dependency injection (<<chapter_13_dependency_injection>>):: 195 | We tidy up our explicit and implicit dependencies and implement a 196 | simple dependency injection framework. 197 | 198 | 199 | ==== Additional Content 200 | 201 | How do I get there from here? (<<epilogue_1_how_to_get_there_from_here>>):: 202 | Implementing architectural patterns always looks easy when you show a simple 203 | example, starting from scratch, but many of you will probably be wondering how 204 | to apply these principles to existing software. We'll provide a 205 | few pointers in the epilogue and some links to further reading. 206 | 207 | 208 | 209 | === Example Code and Coding Along 210 | 211 | You're reading a book, but you'll probably agree with us when we say that 212 | the best way to learn about code is to code. We learned most of what we know 213 | from pairing with people, writing code with them, and learning by doing, and 214 | we'd like to re-create that experience as much as possible for you in this book. 215 | 216 | As a result, we've structured the book around a single example project 217 | (although we do sometimes throw in other examples). We'll build up this project as the chapters progress, as if you've paired with us and 218 | we're explaining what we're doing and why at each step. 219 | 220 | But to really get to grips with these patterns, you need to mess about with the 221 | code and get a feel for how it works. You'll find all the code on 222 | GitHub; each chapter has its own branch. You can find https://github.com/cosmicpython/code/branches/all[a list] of the branches on GitHub as well. 223 | 224 | [role="pagebreak-before"] 225 | Here are three ways you might code along with the book: 226 | 227 | * Start your own repo and try to build up the app as we do, following the 228 | examples from listings in the book, and occasionally looking to our repo 229 | for hints. A word of warning, however: if you've read Harry's previous book 230 | and coded along with that, you'll find that this book requires you to figure out more on 231 | your own; you may need to lean pretty heavily on the working versions on GitHub. 232 | 233 | * Try to apply each pattern, chapter by chapter, to your own (preferably 234 | small/toy) project, and see if you can make it work for your use case. This 235 | is high risk/high reward (and high effort besides!). It may take quite some 236 | work to get things working for the specifics of your project, but on the other 237 | hand, you're likely to learn the most. 238 | 239 | * For less effort, in each chapter we outline an "Exercise for the Reader," 240 | and point you to a GitHub location where you can download some partially finished 241 | code for the chapter with a few missing parts to write yourself. 242 | 243 | Particularly if you're intending to apply some of these patterns in your own 244 | projects, working through a simple example is a great way to 245 | safely practice. 246 | 247 | TIP: At the very least, do a `git checkout` of the code from our repo as you 248 | read each chapter. Being able to jump in and see the code in the context of 249 | an actual working app will help answer a lot of questions as you go, and 250 | makes everything more real. You'll find instructions for how to do that 251 | at the beginning of each chapter. 252 | 253 | 254 | === License 255 | 256 | The code (and the online version of the book) is licensed under a Creative 257 | Commons CC BY-NC-ND license, which means you are free to copy and share it with 258 | anyone you like, for non-commercial purposes, as long as you give attribution. 259 | If you want to re-use any of the content from this book and you have any 260 | worries about the license, contact O'Reilly at pass:[<a class="email" 261 | href="mailto:permissions@oreilly.com"><em>permissions@oreilly.com</em></a>]. 262 | 263 | The print edition is licensed differently; please see the copyright page. 264 | 265 | 266 | === Conventions Used in This Book 267 | 268 | The following typographical conventions are used in this book: 269 | 270 | _Italic_:: Indicates new terms, URLs, email addresses, filenames, and file extensions. 271 | 272 | +Constant width+:: Used for program listings, as well as within paragraphs to refer to program elements such as variable or function names, databases, data types, environment variables, statements, and keywords. 273 | 274 | **`Constant width bold`**:: Shows commands or other text that should be typed literally by the user. 275 | 276 | _++Constant width italic++_:: Shows text that should be replaced with user-supplied values or by values determined by context. 277 | 278 | 279 | [TIP] 280 | ==== 281 | This element signifies a tip or suggestion. 282 | ==== 283 | 284 | [NOTE] 285 | ==== 286 | This element signifies a general note. 287 | ==== 288 | 289 | [WARNING] 290 | ==== 291 | This element indicates a warning or caution. 292 | ==== 293 | 294 | === O'Reilly Online Learning 295 | 296 | [role = "ormenabled"] 297 | [NOTE] 298 | ==== 299 | For more than 40 years, pass:[<a href="http://oreilly.com" class="orm:hideurl"><em class="hyperlink">O’Reilly Media</em></a>] has provided technology and business training, knowledge, and insight to help companies succeed. 300 | ==== 301 | 302 | Our unique network of experts and innovators share their knowledge and expertise through books, articles, conferences, and our online learning platform. O’Reilly’s online learning platform gives you on-demand access to live training courses, in-depth learning paths, interactive coding environments, and a vast collection of text and video from O'Reilly and 200+ other publishers. For more information, please visit pass:[<a href="http://oreilly.com" class="orm:hideurl"><em>http://oreilly.com</em></a>]. 303 | 304 | === How to Contact O'Reilly 305 | 306 | Please address comments and questions concerning this book to the publisher: 307 | 308 | ++++ 309 | <ul class="simplelist"> 310 | <li>O’Reilly Media, Inc.</li> 311 | <li>1005 Gravenstein Highway North</li> 312 | <li>Sebastopol, CA 95472</li> 313 | <li>800-998-9938 (in the United States or Canada)</li> 314 | <li>707-829-0515 (international or local)</li> 315 | <li>707-829-0104 (fax)</li> 316 | </ul> 317 | ++++ 318 | 319 | We have a web page for this book, where we list errata, examples, and any additional information. You can access this page at https://oreil.ly/architecture-patterns-python[]. 320 | 321 | ++++ 322 | <!--Don't forget to update the link above.--> 323 | ++++ 324 | 325 | Email pass:[<a class="email" href="mailto:bookquestions@oreilly.com"><em>bookquestions@oreilly.com</em></a>] to comment or ask technical questions about this book. 326 | 327 | For more information about our books, courses, conferences, and news, see our website at link:$http://www.oreilly.com$[]. 328 | 329 | Find us on Facebook: link:$http://facebook.com/oreilly$[] 330 | 331 | Follow us on Twitter: link:$http://twitter.com/oreillymedia$[] 332 | 333 | Watch us on YouTube: link:$http://www.youtube.com/oreillymedia$[] 334 | 335 | === Acknowledgments 336 | 337 | To our tech reviewers, David Seddon, Ed Jung, and Hynek Schlawack: we absolutely 338 | do not deserve you. You are all incredibly dedicated, conscientious, and 339 | rigorous. Each one of you is immensely smart, and your different points of 340 | view were both useful and complementary to each other. Thank you from the 341 | bottom of our hearts. 342 | 343 | Gigantic thanks also to all our readers so far for their comments and 344 | suggestions: 345 | Ian Cooper, Abdullah Ariff, Jonathan Meier, Gil Gonçalves, Matthieu Choplin, 346 | Ben Judson, James Gregory, Łukasz Lechowicz, Clinton Roy, Vitorino Araújo, 347 | Susan Goodbody, Josh Harwood, Daniel Butler, Liu Haibin, Jimmy Davies, Ignacio 348 | Vergara Kausel, Gaia Canestrani, Renne Rocha, pedroabi, Ashia Zawaduk, Jostein 349 | Leira, Brandon Rhodes, Jazeps Basko, simkimsia, Adrien Brunet, Sergey Nosko, 350 | Dmitry Bychkov, dayres2, programmer-ke, asjhita, Filip Lajszczak, 351 | and many more; our apologies if we missed you on this list. 352 | 353 | Super-mega-thanks to our editor Corbin Collins for his gentle chivvying, and 354 | for being a tireless advocate of the reader. Similarly-superlative thanks to 355 | the production staff, Katherine Tozer, Sharon Wilkey, Ellen Troutman-Zaig, and 356 | Rebecca Demarest, for your dedication, professionalism, and attention to 357 | detail. This book is immeasurably improved thanks to you. 358 | 359 | Any errors remaining in the book are our own, naturally. 360 | -------------------------------------------------------------------------------- /print_figure_numbers_xref_to_image_filenames.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from pathlib import Path 3 | import re 4 | 5 | for path in sorted(Path(__file__).absolute().parent.glob('*.asciidoc')): 6 | images = re.findall(r'::images/(\w+\.png)', path.read_text()) 7 | if not images: 8 | continue 9 | chapter_no = re.search(r'chapter_(\d\d)', str(path)) 10 | if chapter_no: 11 | chapter_no = str(int(chapter_no.group(1))) 12 | else: 13 | chapter_no = '??' 14 | print(path.name) 15 | for ix, image in enumerate(images): 16 | print(f' Figure {chapter_no}.{ix+1}: {image}') 17 | -------------------------------------------------------------------------------- /proposal.md: -------------------------------------------------------------------------------- 1 | # Proposed Book Title: 2 | 3 | * The Clean Architecture in Python? 4 | * Ports and Adapters with Python? 5 | * Enterprise Design Patterns in Python? 6 | * Enterprise Software Architecture in Python? 7 | * Software Architecture in Python? 8 | 9 | ## Proposed Book Subtitle: 10 | 11 | *How to apply DDD, Ports and Adapters and more enterprise architecture design patterns in a Pythonic way.* 12 | 13 | # Author(s): 14 | 15 | Bob Gregory, Harry Percival 16 | 17 | > Author title(s) and affiliation(s): 18 | 19 | Lead Architect and Software Developer (respectively) at MADE.com 20 | 21 | > Preferred mailing address(es): 22 | > Preferred phone number: 23 | > Preferred Email address(es): 24 | harry.percival@gmail.com, bob.gregory@made.com 25 | > Author Platform details: 26 | > Author biography and LinkedIn profile: 27 | 28 | https://uk.linkedin.com/in/harry-percival-588a35 29 | https://uk.linkedin.com/in/bobthemighty 30 | 31 | 32 | > Author public speaking samples (YouTube, etc.): 33 | 34 | https://www.youtube.com/watch?v=tFalO9KdCDM 35 | https://skillsmatter.com/skillscasts/12182-event-sourcing-101 36 | 37 | > Author Web site/blog/Twitter: 38 | 39 | https://io.made.com/ 40 | http://www.obeythetestinggoat.com/ 41 | 42 | 43 | # Why are you the best person to write this book? 44 | 45 | Harry has already written one excellent Python book for O'Reilly, this will make a nice sequel (in fact it covers some of the further reading subjects suggested in the final chapter of said book). 46 | Bob knows more than Harry about the subject matter though. 47 | 48 | 49 | # Book Summary: 50 | 51 | > In one sentence, tell us why the audience will want to buy your book. 52 | 53 | The Python world is increasingly interested in software architecture and design, and there are no good Python-specific books on the topic yet. 54 | 55 | 56 | > Summarize what the book is about, like you would pitch it to a potential reader on the back cover. What makes your book unique in the marketplace? 57 | 58 | As Python grows in popularity as a language, typical projects are getting larger and more complex, and issues of software design and architecture become more salient. Patterns like "Ports and Adapters", as well Domain-Driven Design, Event-Driven programming, Command-Query Responsibility Segregation, which are well known in more "enterprisey" communities like Java and C#, are generating more interest in the Python community, but it's not always easy to see how these patterns translate into Python. (reviewing the classic "Gang of Four" design patterns book for example leads to the conclusion that half the patterns are artifacts of the Java/C++ syntax, and are simply not necessary in the more flexible and dynamic world of Python). 59 | 60 | In the Python world, we often quote the Zen of Python: "there should be one--preferably on only one--obvious way to do it". Unfortunately, as project complexity grows, the most obvious way of doing things isn't always the way that helps you manage complexity and evolving requirements. 61 | 62 | This book will provide an introduction to proven architectural design patterns that help you manage complexity, using concrete examples written in Python. It will explain how to avoid some of the unnecessary particularities of Java and C# syntax and implement these patterns in a "Pythonic" way. 63 | 64 | 65 | # Technology summary. 66 | 67 | > How would you characterize the technology’s stage of development? (Put an X in the column next to the stage that best applies.) 68 | 69 | In the enterprise world: mature. In the Python world: developing. 70 | 71 | 72 | > Briefly explain the technology and why it is important. 73 | 74 | Design patterns and software architecture are well established topics in the enterprise software development world, but much less so in the Python world. As Python matures, translating these topics across is becoming more and more important. 75 | 76 | 77 | 78 | # Audience: 79 | 80 | > Explain who the primary audience is for your book. What professional positions does this audience hold? What knowledge do you assume of this audience? What books can you assume they have read? What skills can you assume they have mastered? 81 | 82 | This is an intermediate-level book. It will be of interest to anyone working on codebases of more than moderate complexity, and anyone with an interest in applying architectural patterns common in the C#/Java world to Python. It will also be of interest to software architects and developers coming from those communities and looking to adapt to the Python world. Finally the aim will be to make this the most accessible, engaging, and concrete introduction to the architectural concepts involved, such that programmers from any background will turn to it as a first resource. 83 | 84 | These people might have read: 85 | 86 | - my first book, and be wondering where to go next 87 | - classics like the Evans or Vernon DDD books, or Martin Fowler's "Patterns of Enterprise Application Architecture", and be wondering how to translate those concepts to the Python world 88 | 89 | > Please estimate as best you can how many people will use this technology? Please state any applicable statistics (e.g., web searches, web site traffic, blogs) indicating market use or market growth. 90 | 91 | Hard to say. salesrank data from amazon suggest books like "Buiding evolutionary architectures" (O'Reilly, https://www.novelrank.com/asin/1491986360) and "Patterns of Enterprise Application Architecture" (a classic, https://www.novelrank.com/asin/0321127420) are selling well (compared to my existing book lol) 92 | 93 | Other than that, informally, several people have reached out to us in response to our existing 5-part blog post series on the made.com blog, saying how much they'd like to see that content extended to book length, and complaining about the lack of similar resources in the Python world 94 | 95 | 96 | 97 | > Please provide some scenarios that indicate how the audience will use your book. For example, will readers refer to it daily as a reference? Will they read it once to learn the concepts and then refer to it occasionally? 98 | 99 | I expect most readers will read it all the way through once. Some may decide to follow along with the code examples in some or all of the chapters. Then, they are likely to come back and look at the code examples in more detail as they come to try and implement the various patterns in their day-to-day jobs. 100 | 101 | > Use the following table to describe how the audience for your book typically gets information and where it looks for guidance and leadership (list top five choices). 102 | 103 | > What web sites or blogs do they read? 104 | 105 | my blog, hacker news, /r/python, the made blog, follow on twitter bernhardt, fowler, beck, uncle martin... 106 | 107 | > What publications do they read (e.g., magazines, journals, newspapers)? 108 | 109 | see above. 110 | 111 | > What conferences do they attend? 112 | 113 | pycon(s), oreilly software architecture conference, oscon, fosdem, 114 | 115 | 116 | > Who are the leaders and key influencers in the field who would review or endorse your book? 117 | 118 | I could reach out via some contacts at Thoughtworks to see if Martin Fowler might give it a read. That would be a great name to have on the cover. 119 | 120 | In the Python world, Harry's existing Python contacts should be prepared to take a look -- Kenneth Reitz, Gary Bernhanrdt, Michael Foord. 121 | 122 | 123 | 124 | 125 | 126 | # Key Topic Coverage: 127 | 128 | > What are the top five topics that will be covered in the book? Why are they the top five? 129 | 130 | - the dependency inversion principle 131 | - ports and adapters 132 | - domain-driven design (DDD) 133 | - CQRS (command/query responsibility segregation) 134 | - event-driven architectures (and link to microservices) 135 | 136 | These are the most popular and recognisable words that people will recognise or have heard of, and appreciate seeing a concrete illustration and discussion of, in the Python world. 137 | 138 | > What problems does this book solve for its users? 139 | 140 | - How do I deal with increasing complexity as my application grows? 141 | - How can I learn about enterprise software design principles without wading through overcomplicated Java/C++ syntax? 142 | - Where can I find concrete examples rather than abstract discussion? 143 | 144 | > List the four or five topics covered or features included that will provide the greatest benefit to readers or will be the most likely to excite them? 145 | 146 | see above, plus: 147 | 148 | - How do I separate my domain model from infrastructure and integration layers? 149 | - Concretely, what is a Port and what is an Adapter in Python? Does the distinction matter? 150 | - What are the expected benefits of this sort of architecture? When is it worth implementing? 151 | - How do these patterns complement a microservices approach? 152 | 153 | > SEO terms for topics covered: 154 | 155 | see bullets above, plus "clean architecture", "hexagonal architecture", "functional core imperative shell",... 156 | 157 | 158 | # Other Book Features and Video Offerings: 159 | 160 | > Is there a companion web site? If so, what do you plan to include on the site? Would you be willing to participate in video offerings as well as workshops and training seminars? 161 | 162 | - a website with access to source code examples, and follow-on blog posts and materials is likely. 163 | - it could also include "alternative implementations" showing some other ways of achieving the same goals, compared to the examples given in the book. 164 | - not entirely sure that video is the right medium for getting these kinds of concepts across, but up for trying. 165 | 166 | 167 | 168 | # Competition: 169 | 170 | > What books or online resources compete with this book? Please list the title and author. In each case, how will your book be different or better in timing, content, coverage, approach, or tone? 171 | 172 | <research some books and provide their PageRank?> 173 | 174 | DDD book? 175 | patterns of enteprise arch? 176 | 177 | 178 | 179 | # Book Outline (old): 180 | 181 | 1. the simple approach 182 | 2. domain model 183 | 3. dependency injection and inversion of control 184 | 4. persistence and unit of work 185 | 5. commands & events, handlers, message bus 186 | 6. (ports and) adapters 187 | 7. command-query separation. 188 | 8. the proof is in the pudding 1: implementing a new feature 189 | 9. the proof is in the pudding 2: refactoring an infrastructure layer 190 | 10. comparison with the simple approach 191 | 11. conclusions: when to use these patterns 192 | A. appendix: functional core, imperative shell 193 | B. appendix: more design patterns 194 | C. alternatives 195 | 196 | 197 | # Specs and Schedule: 198 | 199 | > How many pages do you expect the book to be? 200 | 200-250? 201 | 202 | > What use you will make of illustrations or screenshots? Approximately how many illustrations do you anticipate using? 203 | 204 | Several code listings per chapter, and maybe a few diagrams which we can produce ourselves... 205 | 206 | > What special considerations apply to your plans for the book, including unusual format, use of color, hard-to-get illustrations, or anything else calling for unusual resources? 207 | 208 | nothing extraordinary is anticipated 209 | 210 | > When do you anticipate delivering a complete draft of the manuscript or technical review? 211 | 212 | 9 months' time? 213 | 214 | -------------------------------------------------------------------------------- /push-branches.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import subprocess 4 | from pathlib import Path 5 | from chapters import CHAPTERS, NO_EXERCISE, STANDALONE 6 | 7 | processes = [] 8 | 9 | for chapter in CHAPTERS + STANDALONE: 10 | print('pushing', chapter) 11 | processes.append(subprocess.Popen( 12 | ['git', 'push', '-v', '--force-with-lease', 'origin', chapter], 13 | cwd=Path(__file__).parent / 'code', 14 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, 15 | )) 16 | if chapter in NO_EXERCISE: 17 | continue 18 | exercise_branch = f'{chapter}_exercise' 19 | print('pushing', exercise_branch) 20 | processes.append(subprocess.Popen( 21 | ['git', 'push', '-v', '--force-with-lease', 'origin', exercise_branch], 22 | cwd=Path(__file__).parent / 'code', 23 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, 24 | )) 25 | 26 | print('pushing master') 27 | processes.append(subprocess.Popen( 28 | ['git', 'push', '-v', '--force-with-lease', 'origin', 'master'], 29 | cwd=Path(__file__).parent / 'code', 30 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, 31 | )) 32 | 33 | for p in processes: 34 | stdout, stderr = p.communicate() 35 | print(stdout) 36 | print(stderr) 37 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --tb=short 3 | -------------------------------------------------------------------------------- /rebase-appendices.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | set -ex 3 | 4 | cd code 5 | git co appendix_django 6 | git irebase chapter_06_uow 7 | 8 | git co appendix_csvs 9 | git irebase chapter_06_uow 10 | 11 | git co master 12 | -------------------------------------------------------------------------------- /rebase-chapters.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | set -ex 3 | 4 | if [[ $# -eq 0 ]] ; then 5 | echo 'need commit to rebase from' 6 | exit 0 7 | fi 8 | 9 | cd code 10 | git co chapter_13_dependency_injection 11 | git irebase $1 12 | 13 | git co master 14 | git reset --hard chapter_13_dependency_injection 15 | 16 | cd .. 17 | -------------------------------------------------------------------------------- /render-diagrams.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import re 3 | import sys 4 | import tempfile 5 | import subprocess 6 | from pathlib import Path 7 | from lxml import html 8 | 9 | IMAGES_DIR = Path(__file__).absolute().parent / 'images' 10 | 11 | 12 | def all_chapter_names(): 13 | for fn in sorted(Path(__file__).absolute().parent.glob('*.html')): 14 | chapter_name = fn.name.replace('.html', '') 15 | if chapter_name == 'book': 16 | continue 17 | yield chapter_name 18 | 19 | def main(paths): 20 | print(paths) 21 | if paths: 22 | chapter_names = [p.replace('.html', '').replace('.asciidoc', '') for p in paths] 23 | else: 24 | chapter_names = all_chapter_names() 25 | for chapter_name in chapter_names: 26 | render_images(chapter_name) 27 | 28 | 29 | def render_images(chapter_name): 30 | print('Rendering images for', chapter_name) 31 | raw_contents = Path(f'{chapter_name}.html').read_text() 32 | parsed_html = html.fromstring(raw_contents) 33 | 34 | for image_block in parsed_html.cssselect('.imageblock'): 35 | [img] = image_block.cssselect('img') 36 | image_id = img.get('src').replace('images/', '').replace('.png', '') 37 | print(image_id) 38 | 39 | parent = image_block.getparent() 40 | next_sibling_pos = parent.index(image_block) + 1 41 | try: 42 | next_element = parent[next_sibling_pos] 43 | except IndexError: 44 | continue 45 | if 'image-source' in next_element.classes: 46 | code = next_element.cssselect('pre')[0].text 47 | render_image(code, image_id) 48 | 49 | INCLUDES = [ 50 | 'images/C4_Context.puml', 51 | 'images/C4_Component.puml', 52 | ] 53 | 54 | def _add_dots(source, image_id): 55 | lines = source.splitlines() 56 | assert lines[0].startswith('[') 57 | assert image_id in lines[0] 58 | plantuml_cfg = str(Path('plantuml.cfg').absolute()) 59 | lines[0] = lines[0].replace('config=plantuml.cfg', f'config={plantuml_cfg}') 60 | lines[0] = re.sub(r'\[ditaa, (\w+)\]', r'[ditaa, \1, scale=4]', lines[0]) 61 | for ix, l in enumerate(lines): 62 | if include := next((i for i in INCLUDES if i in l), None): 63 | lines[ix] = l.replace(include, str(Path(include).absolute())) 64 | lines.insert(1, '....') 65 | lines.append('....') 66 | return '\n'.join(lines) 67 | 68 | 69 | def render_image(source, image_id): 70 | source = _add_dots(source, image_id) 71 | print(source) 72 | target = Path(f'images/{image_id}.png') 73 | if target.exists(): 74 | target.unlink() 75 | tf = Path(tempfile.NamedTemporaryFile().name) 76 | tf.write_text(source) 77 | cmd = ['asciidoctor', '-r', 'asciidoctor-diagram', '-a', f'imagesoutdir={IMAGES_DIR}', str(tf)] 78 | print(' '.join(cmd)) 79 | subprocess.run(cmd, check=True) 80 | 81 | 82 | if __name__ == '__main__': 83 | main(sys.argv[1:]) 84 | -------------------------------------------------------------------------------- /renumber-chapters.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import subprocess 3 | from pathlib import Path 4 | 5 | MOVES = [ 6 | # change these as desired 7 | ('chapter_04b_high_gear_low_gear', 'chapter_05_high_gear_low_gear'), 8 | ('chapter_05_uow', 'chapter_06_uow'), 9 | ('chapter_06_aggregate', 'chapter_07_aggregate'), 10 | ("chapter_07_events_and_message_bus", "chapter_08_events_and_message_bus"), 11 | ("chapter_08_all_messagebus", "chapter_09_all_messagebus"), 12 | ("chapter_09_commands", "chapter_10_commands"), 13 | ("chapter_10_external_events", "chapter_11_external_events"), 14 | ("chapter_11_cqrs", "chapter_12_cqrs"), 15 | ("chapter_12_dependency_injection", "chapter_13_dependency_injection"), 16 | ] 17 | 18 | for frm, to in MOVES: 19 | subprocess.run(['git', 'mv', f'{frm}.asciidoc', f'{to}.asciidoc'], check=True) 20 | 21 | sources = list(Path(__file__).absolute().parent.glob('*.asciidoc')) 22 | otherthings = [ 23 | 'chapters.py', 24 | 'atlas.json', 25 | 'Readme.md', 26 | 'rebase-chapters.sh', 27 | 'rebase-appendices.sh', 28 | ] 29 | for frm, to in MOVES: 30 | subprocess.run( 31 | ['sed', '-i', f's/{frm}/{to}/g'] + sources + otherthings, 32 | check=True, 33 | ) 34 | 35 | input('base repo done, ready to do submodules') 36 | 37 | for frm, to in MOVES: 38 | code = Path(__file__).absolute().parent / 'code' 39 | subprocess.run(['git', 'branch', '-m', frm, to], cwd=code) 40 | subprocess.run(['git', 'branch', '--unset-upstream', to], cwd=code) # untested 41 | subprocess.run(['git', 'push', 'origin', f':{frm}'], cwd=code) 42 | subprocess.run(['git', 'checkout', to], cwd=code) 43 | subprocess.run(['git', 'push', '-u', 'origin', to], cwd=code) 44 | from chapters import NO_EXERCISE 45 | if to not in NO_EXERCISE: 46 | # untested 47 | subprocess.run( 48 | ['git', 'branch', '-m', f'{frm}_exercise', f'{to}_exercise'], 49 | cwd=code, check=True, 50 | ) 51 | 52 | input(f'{frm}->{to} done in theory') 53 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | lxml 2 | cssselect 3 | pytest-icdiff 4 | pylint 5 | mypy 6 | -r code/requirements.txt 7 | -------------------------------------------------------------------------------- /reset-exercise-branches.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import subprocess 3 | from pathlib import Path 4 | from chapters import CHAPTERS 5 | 6 | 7 | def run(cmds): 8 | print(' '.join(cmds)) 9 | p = subprocess.run( 10 | cmds, 11 | cwd=Path(__file__).parent / 'code', 12 | capture_output=True, 13 | ) 14 | if p.returncode: 15 | raise Exception(p.stderr.decode()) 16 | output = p.stdout.decode() 17 | print(output) 18 | return output 19 | 20 | 21 | all_branches = run(['git', 'branch', '-a'],) 22 | 23 | for chapter in CHAPTERS: 24 | exercise_chapter = f'{chapter}_exercise' 25 | if exercise_chapter not in all_branches: 26 | continue 27 | run(['git', 'checkout', exercise_chapter]) 28 | run(['git', 'reset', '--hard', f'origin/{exercise_chapter}']) 29 | run(['git', 'checkout', 'master']) 30 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=redefined-outer-name 2 | import re 3 | import subprocess 4 | from contextlib import contextmanager 5 | from dataclasses import dataclass 6 | from pathlib import Path 7 | from lxml import html 8 | import pytest 9 | from chapters import CHAPTERS, BRANCHES, STANDALONE, NO_EXERCISE 10 | 11 | 12 | 13 | def all_branches(): 14 | return subprocess.run( 15 | ['git', 'branch', '-a'], 16 | cwd=Path(__file__).parent / 'code', 17 | stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 18 | check=True 19 | ).stdout.decode().split() 20 | 21 | def git_log(chapter): 22 | return subprocess.run( 23 | ['git', 'log', chapter, '--oneline', '--decorate'], 24 | cwd=Path(__file__).parent / 'code', 25 | stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 26 | check=True 27 | ).stdout.decode() 28 | 29 | @pytest.fixture(scope='session') 30 | def master_log(): 31 | return git_log('master') 32 | 33 | @pytest.mark.parametrize('chapter', CHAPTERS) 34 | def test_master_has_all_chapters_in_its_history(master_log, chapter): 35 | if chapter in BRANCHES: 36 | return 37 | assert f'{chapter})' in master_log 38 | 39 | @pytest.mark.parametrize('chapter', CHAPTERS) 40 | def test_exercises_for_reader(chapter): 41 | exercise_branch = f'{chapter}_exercise' 42 | branches = all_branches() 43 | if chapter in NO_EXERCISE: 44 | if exercise_branch in branches: 45 | pytest.fail(f'looks like there is an exercise for {chapter} after all!') 46 | else: 47 | pytest.xfail(f'{chapter} has no exercise yet') 48 | return 49 | assert exercise_branch in branches 50 | assert f'{chapter})' in git_log(exercise_branch), f'Exercise for {chapter} not up to date' 51 | 52 | def previous_chapter(chapter): 53 | chapter_no = CHAPTERS.index(chapter) 54 | if chapter_no == 0: 55 | return None 56 | previous = CHAPTERS[chapter_no - 1] 57 | if previous in BRANCHES: 58 | previous = CHAPTERS[chapter_no - 2] 59 | return previous 60 | 61 | @pytest.mark.parametrize('chapter', CHAPTERS) 62 | def test_each_chapter_follows_the_last(chapter): 63 | previous = previous_chapter(chapter) 64 | if previous is None: 65 | return 66 | assert f'{previous})' in git_log(chapter), f'{chapter} did not follow {previous}' 67 | 68 | 69 | @pytest.mark.parametrize('chapter', CHAPTERS + STANDALONE) 70 | def test_chapter(chapter): 71 | for listing in parse_listings(chapter): 72 | check_listing(listing, chapter) 73 | 74 | 75 | @contextmanager 76 | def checked_out(chapter): 77 | subprocess.run( 78 | ['git', 'checkout', f'{chapter}'], 79 | cwd=Path(__file__).parent / 'code', 80 | stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 81 | check=True 82 | ) 83 | try: 84 | yield 85 | 86 | finally: 87 | subprocess.run( 88 | ['git', 'checkout', '-'], 89 | cwd=Path(__file__).parent / 'code', 90 | stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 91 | check=True 92 | ) 93 | 94 | 95 | def tree_for_branch(chapter_name): 96 | with checked_out(chapter_name): 97 | return subprocess.run( 98 | ['tree', '-v', '-I', '__pycache__|*.egg-info'], 99 | cwd=Path(__file__).parent / 'code', 100 | stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 101 | check=True 102 | ).stdout.decode() 103 | 104 | 105 | def check_listing(listing, chapter): 106 | if 'tree' in listing.classes: 107 | actual_contents = tree_for_branch(chapter) 108 | elif 'non-head' in listing.classes: 109 | actual_contents = file_contents_for_tag( 110 | listing.filename, chapter, listing.tag, 111 | ) 112 | elif 'existing' in listing.classes: 113 | actual_contents = file_contents_for_previous_chapter( 114 | listing.filename, chapter, 115 | ) 116 | elif listing.is_diff: 117 | actual_contents = diff_for_tag( 118 | listing.filename, chapter, listing.tag, 119 | ) 120 | 121 | else: 122 | actual_contents = file_contents_for_branch(listing.filename, chapter) 123 | actual_lines = actual_contents.split('\n') 124 | 125 | if '...' in listing.contents: 126 | for section in re.split(r'#?\.\.\.', listing.fixed_contents): 127 | lines = section.splitlines() 128 | if section.strip() not in actual_contents: 129 | assert lines == actual_lines, \ 130 | f'section from [{listing.tag}] not found within actual' 131 | 132 | elif listing.fixed_contents not in actual_contents: 133 | assert listing.lines == actual_lines, \ 134 | f'listing [{listing.tag}] not found within actual' 135 | 136 | 137 | 138 | @dataclass 139 | class Listing: 140 | filename: str 141 | tag: str 142 | contents: str 143 | classes: list 144 | is_diff: bool 145 | 146 | callouts = re.compile(r' #?(\(\d\) ?)+#39;, flags=re.MULTILINE) 147 | callouts_alone = re.compile(r'^\(\d\)#39;) 148 | 149 | @property 150 | def fixed_contents(self): 151 | fixed = self.contents 152 | fixed = self.callouts.sub('', fixed) 153 | fixed = '\n'.join( 154 | l for l in fixed.splitlines() 155 | if not self.callouts_alone.match(l) 156 | ) 157 | return fixed 158 | 159 | @property 160 | def lines(self): 161 | return self.fixed_contents.split('\n') 162 | 163 | 164 | def parse_listings(chapter_name): 165 | raw_contents = Path(f'{chapter_name}.html').read_text() 166 | parsed_html = html.fromstring(raw_contents) 167 | 168 | for listing_node in parsed_html.cssselect('.exampleblock'): 169 | [block_node] = listing_node.cssselect('.listingblock') 170 | classes = block_node.get('class').split() 171 | if 'skip' in classes: 172 | continue 173 | 174 | if 'tree' in classes: 175 | filename = None 176 | else: 177 | [title_node] = listing_node.cssselect('.title') 178 | title = title_node.text_content() 179 | print('found listing', title) 180 | try: 181 | filename = re.search(r'.+ \((.+)\)', title).group(1) 182 | except AttributeError as e: 183 | raise AssertionError(f'Could not find filename in title {title}') from e 184 | 185 | is_diff = bool(listing_node.cssselect('code[data-lang="diff"]')) 186 | tag = listing_node.get('id') 187 | 188 | [code_node] = block_node.cssselect('.content pre') 189 | yield Listing( 190 | filename, tag, contents=code_node.text_content(), classes=classes, 191 | is_diff=is_diff 192 | ) 193 | 194 | 195 | def file_contents_for_branch(filename, chapter_name): 196 | return subprocess.run( 197 | ['git', 'show', f'{chapter_name}:{filename}'], 198 | cwd=Path(__file__).parent / 'code', 199 | stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 200 | check=True 201 | ).stdout.decode() 202 | 203 | def file_contents_for_previous_chapter(filename, chapter_name): 204 | previous = previous_chapter(chapter_name) 205 | return file_contents_for_branch(filename, previous) 206 | 207 | def file_contents_for_tag(filename, chapter_name, tag): 208 | output = subprocess.run( 209 | ['git', 'show', f'{chapter_name}^{{/\\[{tag}\\]}}:{filename}'], 210 | cwd=Path(__file__).parent / 'code', 211 | stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 212 | check=True 213 | ).stdout.decode() 214 | assert output.strip(), f'no commit found for [{tag}]' 215 | return output 216 | 217 | def diff_for_tag(filename, chapter_name, tag): 218 | if tag.endswith('_diff'): 219 | tag = tag[:-5] 220 | output = subprocess.run( 221 | ['git', 'show', f'{chapter_name}^{{/\\[{tag}\\]}}', '--', filename], 222 | cwd=Path(__file__).parent / 'code', 223 | stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 224 | check=True 225 | ).stdout.decode() 226 | assert output.strip(), f'no commit found for [{tag}]' 227 | return '\n'.join(l.rstrip() for l in output.splitlines()) 228 | -------------------------------------------------------------------------------- /theme/asciidoctor-clean.custom.css: -------------------------------------------------------------------------------- 1 | /* Asciidoctor default stylesheet | MIT License | https://asciidoctor.org */ 2 | 3 | @import url("//fonts.googleapis.com/css?family=Noto+Sans:300,600italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400,700"); 4 | @import url(//asciidoctor.org/stylesheets/asciidoctor.css); /* Default asciidoc style framework - important */ 5 | 6 | /* customisations by harry */ 7 | 8 | h1, h2, h3, h4, h5, h6 { 9 | position: relative; 10 | } 11 | 12 | a.anchor { 13 | top: 0; 14 | } 15 | 16 | /* hide inline ditaa/plantuml source listings for images */ 17 | .image-source { 18 | display: none 19 | } 20 | /* make formal codeblocks a bit nicer */ 21 | .exampleblock > .content { 22 | padding: 2px; 23 | background-color: white; 24 | border: 0; 25 | margin-bottom: 2em; 26 | } 27 | .exampleblock .title { 28 | text-align: right; 29 | } 30 | 31 | /* prev/next chapter links at bottom of page */ 32 | .prev_and_next_chapter_links { 33 | margin: 10px; 34 | } 35 | .prev_chapter_link { 36 | float: left; 37 | } 38 | .next_chapter_link { 39 | float: right; 40 | } 41 | 42 | 43 | /* a few tweaks to existing styles */ 44 | #toc li { 45 | margin-top: 0.5em; 46 | } 47 | 48 | #footnotes hr { 49 | width: 100%; 50 | } 51 | 52 | /* end customisations by harry */ 53 | 54 | 55 | /* CUSTOMISATIONS */ 56 | 57 | /* Change the values in root for quick customisation. If you want even more fine grain... venture further. */ 58 | 59 | :root{ 60 | --maincolor:#FFFFFF; 61 | --primarycolor:#2c3e50; 62 | --secondarycolor:#ba3925; 63 | --tertiarycolor: #186d7a; 64 | --sidebarbackground:#CCC; 65 | --linkcolor:#b71c1c; 66 | --linkcoloralternate:#f44336; 67 | --white:#FFFFFF; 68 | --black:#000000; 69 | } 70 | 71 | /* Text styles */ 72 | h1{color:var(--primarycolor) !important;} 73 | 74 | h2,h3,h4,h5,h6{color:var(--secondarycolor) !important;} 75 | 76 | .title{color:var(--tertiarycolor) !important; font-family:"Noto Sans",sans-serif !important;font-style: normal !important; font-weight: normal !important;} 77 | p{font-family: "Noto Sans",sans-serif !important} 78 | 79 | /* Table styles */ 80 | th{font-family: "Noto Sans",sans-serif !important} 81 | 82 | /* Responsiveness fixes */ 83 | video { 84 | max-width: 100%; 85 | } 86 | 87 | @media all and (max-width: 600px) { 88 | table { 89 | width: 55vw!important; 90 | font-size: 3vw; 91 | } 92 | -------------------------------------------------------------------------------- /theme/epub/epub.css: -------------------------------------------------------------------------------- 1 | /* harry rule to hide inline image sources */ 2 | .image-source { 3 | display: none !important; 4 | } 5 | 6 | /* Custom widths */ 7 | .width-10 { width: 10% !important; } 8 | .width-20 { width: 20% !important; } 9 | .width-30 { width: 30% !important; } 10 | .width-40 { width: 40% !important; } 11 | .width-50 { width: 50% !important; } 12 | .width-60 { width: 60% !important; } 13 | .width-70 { width: 70% !important; } 14 | .width-75 { width: 75% !important; } 15 | .width-80 { width: 80% !important; } 16 | .width-90 { width: 90% !important; } 17 | .width-full, 18 | .width-100 { width: 100% !important; } 19 | -------------------------------------------------------------------------------- /theme/epub/epub.xsl: -------------------------------------------------------------------------------- 1 | <xsl:stylesheet version="1.0" 2 | xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 3 | xmlns:h="http://www.w3.org/1999/xhtml" 4 | xmlns="http://www.w3.org/1999/xhtml" 5 | exclude-result-prefixes="h"> 6 | 7 | <!-- Add title heading elements for different admonition types that do not already have headings in markup --> 8 | <xsl:param name="add.title.heading.for.admonitions" select="1"/> 9 | 10 | <!-- Override to print example captions without labels--> 11 | <xsl:template match="h:div[@data-type='example']/h:h5" mode="process-heading"> 12 | <p><em> 13 | <xsl:apply-templates/> 14 | </em></p> 15 | </xsl:template> 16 | 17 | <!-- Drop @width attributes from table headers if present --> 18 | <xsl:template match="h:th/@width"/> 19 | 20 | <!-- OVERRIDE FOR ADDING HANDLING FOR EPILOGUE XREFS and FORMAL ELEMENTS--> 21 | <xsl:template match="h:table|h:figure|h:div[@data-type='example']" mode="label.formal.ancestor"> 22 | <xsl:choose> 23 | <!-- For Preface and Introduction, custom label prefixes for formal ancestor 24 | (don't use label.markup template here, as these labels are typically specific to just formal-object context --> 25 | <!--BEGIN OVERRIDE --> 26 | <xsl:when test="ancestor::h:section[@data-type = 'afterword']">E</xsl:when> 27 | <!-- END OVERRIDE--> 28 | 29 | <xsl:when test="ancestor::h:section[@data-type = 'preface']">P</xsl:when> 30 | <xsl:when test="ancestor::h:section[@data-type = 'introduction']">I</xsl:when> 31 | <xsl:otherwise> 32 | <!-- Otherwise, go ahead and use label.markup to get proper label numeral for ancestor --> 33 | <xsl:apply-templates select="(ancestor::h:section[contains(@data-type, 'acknowledgments') or 34 | contains(@data-type, 'afterword') or 35 | contains(@data-type, 'appendix') or 36 | contains(@data-type, 'bibliography') or 37 | contains(@data-type, 'chapter') or 38 | contains(@data-type, 'colophon') or 39 | contains(@data-type, 'conclusion') or 40 | contains(@data-type, 'copyright-page') or 41 | contains(@data-type, 'dedication') or 42 | contains(@data-type, 'foreword') or 43 | contains(@data-type, 'glossary') or 44 | contains(@data-type, 'halftitlepage') or 45 | contains(@data-type, 'index') or 46 | contains(@data-type, 'introduction') or 47 | contains(@data-type, 'preface') or 48 | contains(@data-type, 'titlepage') or 49 | contains(@data-type, 'toc')]| 50 | ancestor::h:div[@data-type = 'part'])[last()]" mode="label.markup"/> 51 | </xsl:otherwise> 52 | </xsl:choose> 53 | </xsl:template> 54 | </xsl:stylesheet> 55 | -------------------------------------------------------------------------------- /theme/epub/layout.html: -------------------------------------------------------------------------------- 1 | {{ doctype }} 2 | <html lang="en" xmlns="http://www.w3.org/1999/xhtml"> 3 | <head> 4 | <title>{{ title }}</title> 5 | <meta charset="utf-8" /> 6 | <meta name="publisher" content="O'Reilly Media, Inc."/> 7 | <meta name="author" content="Harry Percival and Bob Gregory"/> 8 | <meta name="date" content="2020-03-10"/> 9 | <meta name="description" content="As Python continues to grow in popularity, projects are becoming larger and more complex. Many Python developers are now taking an interest in high-level software architecture patterns such as hexagonal/clean architecture, event-driven architecture, and strategic patterns prescribed by domain-driven design (DDD). But translating those patterns into Python isn’t always straightforward. With this practical guide, Harry Percival and Bob Gregory from MADE.com introduce proven architectural design patterns to help Python developers manage application complexity."/> 10 | <meta name="identifier" content="978-1-492-05220-3"/> 11 | </head> 12 | <body data-type="book"> 13 | {{ content }} 14 | </body> 15 | </html> 16 | -------------------------------------------------------------------------------- /theme/mobi/layout.html: -------------------------------------------------------------------------------- 1 | {{ doctype }} 2 | <html lang="en" xmlns="http://www.w3.org/1999/xhtml"> 3 | <head> 4 | <title>{{ title }}</title> 5 | <meta charset="utf-8" /> 6 | <meta name="publisher" content="O'Reilly Media, Inc."/> 7 | <meta name="author" content="Harry Percival and Bob Gregory"/> 8 | <meta name="date" content="2020-03-10"/> 9 | <meta name="description" content="As Python continues to grow in popularity, projects are becoming larger and more complex. Many Python developers are now taking an interest in high-level software architecture patterns such as hexagonal/clean architecture, event-driven architecture, and strategic patterns prescribed by domain-driven design (DDD). But translating those patterns into Python isn’t always straightforward. With this practical guide, Harry Percival and Bob Gregory from MADE.com introduce proven architectural design patterns to help Python developers manage application complexity."/> 10 | <meta name="identifier" content="978-1-492-05220-3"/> 11 | </head> 12 | <body data-type="book"> 13 | {{ content }} 14 | </body> 15 | </html> 16 | -------------------------------------------------------------------------------- /theme/mobi/mobi.css: -------------------------------------------------------------------------------- 1 | /* harry rule to hide inline image sources */ 2 | .image-source { 3 | display: none !important; 4 | } 5 | -------------------------------------------------------------------------------- /theme/mobi/mobi.xsl: -------------------------------------------------------------------------------- 1 | <xsl:stylesheet version="1.0" 2 | xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 3 | xmlns:h="http://www.w3.org/1999/xhtml" 4 | xmlns="http://www.w3.org/1999/xhtml" 5 | exclude-result-prefixes="h"> 6 | 7 | <!-- Add title heading elements for different admonition types that do not already have headings in markup --> 8 | <xsl:param name="add.title.heading.for.admonitions" select="1"/> 9 | 10 | <!-- Override to print example captions without labels--> 11 | <xsl:template match="h:div[@data-type='example']/h:h5" mode="process-heading"> 12 | <p><em> 13 | <xsl:apply-templates/> 14 | </em></p> 15 | </xsl:template> 16 | 17 | <!-- Drop @width attributes from table headers if present --> 18 | <xsl:template match="h:th/@width"/> 19 | 20 | <!-- OVERRIDE FOR ADDING HANDLING FOR EPILOGUE XREFS and FORMAL ELEMENTS--> 21 | <xsl:template match="h:table|h:figure|h:div[@data-type='example']" mode="label.formal.ancestor"> 22 | <xsl:choose> 23 | <!-- For Preface and Introduction, custom label prefixes for formal ancestor 24 | (don't use label.markup template here, as these labels are typically specific to just formal-object context --> 25 | <!--BEGIN OVERRIDE --> 26 | <xsl:when test="ancestor::h:section[@data-type = 'afterword']">E</xsl:when> 27 | <!-- END OVERRIDE--> 28 | 29 | <xsl:when test="ancestor::h:section[@data-type = 'preface']">P</xsl:when> 30 | <xsl:when test="ancestor::h:section[@data-type = 'introduction']">I</xsl:when> 31 | <xsl:otherwise> 32 | <!-- Otherwise, go ahead and use label.markup to get proper label numeral for ancestor --> 33 | <xsl:apply-templates select="(ancestor::h:section[contains(@data-type, 'acknowledgments') or 34 | contains(@data-type, 'afterword') or 35 | contains(@data-type, 'appendix') or 36 | contains(@data-type, 'bibliography') or 37 | contains(@data-type, 'chapter') or 38 | contains(@data-type, 'colophon') or 39 | contains(@data-type, 'conclusion') or 40 | contains(@data-type, 'copyright-page') or 41 | contains(@data-type, 'dedication') or 42 | contains(@data-type, 'foreword') or 43 | contains(@data-type, 'glossary') or 44 | contains(@data-type, 'halftitlepage') or 45 | contains(@data-type, 'index') or 46 | contains(@data-type, 'introduction') or 47 | contains(@data-type, 'preface') or 48 | contains(@data-type, 'titlepage') or 49 | contains(@data-type, 'toc')]| 50 | ancestor::h:div[@data-type = 'part'])[last()]" mode="label.markup"/> 51 | </xsl:otherwise> 52 | </xsl:choose> 53 | </xsl:template> 54 | 55 | </xsl:stylesheet> 56 | -------------------------------------------------------------------------------- /theme/pdf/pdf.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | /*--------Put Your Custom CSS Rules Below--------*/ 4 | /* Globally preventing code blocks from breaking across pages 5 | pre { page-break-inside: avoid; } */ 6 | 7 | /* Reduce font size (STYL-1219) */ 8 | aside.small { font-size: 0.7em !important; } 9 | 10 | /* handling for elements to keep them from breaking across pages */ 11 | .nobreakinside { page-break-inside: avoid; } 12 | 13 | /* Epilogue figure label */ 14 | section[data-type="afterword"] figcaption:before { 15 | content: "Figure E-"counter(FigureNumber)". "; 16 | } 17 | 18 | /*less space for pagebreaks */ 19 | .less_space {margin-top: 0 !important;} 20 | 21 | /* Allow Examples to have less space at top of page (STYL-1266) 22 | section.less_space > h1:first-child { 23 | margin-top: 0 !important; 24 | 25 | } 26 | 27 | aside.less_space > h5:first-child { 28 | margin-top: 0 !important; 29 | 30 | } 31 | 32 | .less_space > h5:first-child { 33 | margin-top: 0 !important; 34 | padding-top: 0 !important; 35 | } 36 | */ 37 | /* Temporary fix to TOC spacing from Tools */ 38 | nav[data-type="toc"] li { 39 | margin-bottom: 0 !important; 40 | } 41 | 42 | /* harry rule to hide inline image sources */ 43 | .image-source { 44 | display: none !important; 45 | } 46 | 47 | /* Removing "Example X" labels from formal code block captions */ 48 | section[data-type="chapter"] div[data-type="example"] h5:before { 49 | content: none; 50 | } 51 | 52 | section[data-type="appendix"] div[data-type="example"] h5:before { 53 | content: none; 54 | } 55 | 56 | section[data-type="preface"] div[data-type="example"] h5:before { 57 | content: none; 58 | } 59 | 60 | div[data-type="part"] div[data-type="example"] h5:before { 61 | content: none; 62 | } 63 | 64 | div[data-type="part"] section[data-type="chapter"] div[data-type="example"] h5:before { 65 | content: none; 66 | } 67 | 68 | div[data-type="part"] section[data-type="appendix"] div[data-type="example"] h5:before { 69 | content: none; 70 | } 71 | 72 | div[data-type="example"] h5 { 73 | text-align: right !important; 74 | font-size: 9pt !important; 75 | margin-bottom: 0.5ex !important; 76 | } 77 | /*--- This oneoff overrides the code in https://github.com/oreillymedia/<name_of_theme>/blob/master/pdf/pdf.css---*/ 78 | 79 | figure div.border-box { border: none; } 80 | 81 | /*----Uncomment to temporarily turn on code-eyballer highlighting (make sure to recomment after you build) 82 | 83 | pre { 84 | background-color: yellow; 85 | } 86 | ---*/ 87 | 88 | /*----Uncomment to turn on automatic code wrapping 89 | 90 | pre { 91 | white-space: pre-wrap; 92 | word-wrap: break-word; 93 | } 94 | ----*/ 95 | 96 | /*----Uncomment to change the TOC start page (set 97 | the number to one page _after_ the one you want; 98 | so 6 to start on v, 8 to start on vii, etc.) 99 | 100 | @page toc:first { 101 | counter-reset: page 6; 102 | } 103 | ----*/ 104 | 105 | /*----Uncomment to fix a bad break in the title 106 | (increase padding value to push down, decrease 107 | value to pull up) 108 | 109 | section[data-type="titlepage"] h1 { 110 | padding-left: 1.5in; 111 | } 112 | ----*/ 113 | 114 | /*----Uncomment to fix a bad break in the subtitle 115 | (increase padding value to push down, decrease 116 | value to pull up) 117 | 118 | section[data-type="titlepage"] h2 { 119 | padding-left: 1in; 120 | } 121 | ----*/ 122 | 123 | /*----Uncomment to fix a bad break in the author names 124 | (increase padding value to push down, decrease 125 | value to pull up) 126 | 127 | section[data-type="titlepage"] p.author { 128 | padding-left: 3in; 129 | } 130 | ----*/ 131 | 132 | /* ----Uncomment to suppress duplicate page numbers in index entries 133 | WARNING: MAY CAUSE PDF BUILDS TO SEGFAULT 134 | 135 | div[data-type="index"] { 136 | -ah-suppress-duplicate-page-number: true; 137 | } 138 | 139 | ----*/ 140 | -------------------------------------------------------------------------------- /theme/pdf/pdf.xsl: -------------------------------------------------------------------------------- 1 | <xsl:stylesheet version="1.0" 2 | xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 3 | xmlns:h="http://www.w3.org/1999/xhtml" 4 | xmlns="http://www.w3.org/1999/xhtml" 5 | exclude-result-prefixes="h"> 6 | 7 | <!-- Do add border div for figure images in animal series --> 8 | <xsl:param name="figure.border.div" select="1"/> 9 | 10 | <!-- This param is required for animal_theme_sass, but not the old animal_theme --> 11 | <!-- Generate separate footnote-call markers, so that we don't 12 | need to rely on AH counters to do footnote numbering --> 13 | <xsl:param name="process.footnote.callouts.only" select="1"/> 14 | 15 | 16 | <xsl:template name="string-replace-all"> 17 | <xsl:param name="text"/> 18 | <xsl:param name="replace"/> 19 | <xsl:param name="by"/> 20 | <xsl:choose> 21 | <xsl:when test="contains($text, $replace)"> 22 | <xsl:value-of select="substring-before($text,$replace)"/> 23 | <xsl:value-of select="$by"/> 24 | <xsl:call-template name="string-replace-all"> 25 | <xsl:with-param name="text" select="substring-after($text,$replace)"/> 26 | <xsl:with-param name="replace" select="$replace"/> 27 | <xsl:with-param name="by" select="$by"/> 28 | </xsl:call-template> 29 | </xsl:when> 30 | <xsl:otherwise> 31 | <xsl:value-of select="$text"/> 32 | </xsl:otherwise> 33 | </xsl:choose> 34 | </xsl:template> 35 | 36 | <xsl:template match="h:img/@src"> 37 | <xsl:choose> 38 | <xsl:when test="contains(., 'callouts/')"> 39 | <xsl:variable name="newtext"> 40 | <xsl:call-template name="string-replace-all"> 41 | <xsl:with-param name="text" select="."/> 42 | <xsl:with-param name="replace" select="'png'"/> 43 | <xsl:with-param name="by" select="'pdf'"/> 44 | </xsl:call-template> 45 | </xsl:variable> 46 | <xsl:attribute name="src"> 47 | <xsl:value-of select="$newtext"/> 48 | </xsl:attribute> 49 | </xsl:when> 50 | <xsl:otherwise> 51 | <xsl:copy> 52 | <xsl:apply-templates select="@*|node()"/> 53 | </xsl:copy> 54 | </xsl:otherwise> 55 | </xsl:choose> 56 | </xsl:template> 57 | 58 | <!-- OVERRIDE FOR ADDING HANDLING FOR EPILOGUE XREFS--> 59 | <xsl:template match="h:table|h:figure|h:div[@data-type='example']" mode="label.formal.ancestor"> 60 | <xsl:choose> 61 | <!-- For Preface and Introduction, custom label prefixes for formal ancestor 62 | (don't use label.markup template here, as these labels are typically specific to just formal-object context --> 63 | <!--BEGIN OVERRIDE --> 64 | <xsl:when test="ancestor::h:section[@data-type = 'afterword']">E</xsl:when> 65 | <!-- END OVERRIDE--> 66 | 67 | <xsl:when test="ancestor::h:section[@data-type = 'preface']">P</xsl:when> 68 | <xsl:when test="ancestor::h:section[@data-type = 'introduction']">I</xsl:when> 69 | <xsl:otherwise> 70 | <!-- Otherwise, go ahead and use label.markup to get proper label numeral for ancestor --> 71 | <xsl:apply-templates select="(ancestor::h:section[contains(@data-type, 'acknowledgments') or 72 | contains(@data-type, 'afterword') or 73 | contains(@data-type, 'appendix') or 74 | contains(@data-type, 'bibliography') or 75 | contains(@data-type, 'chapter') or 76 | contains(@data-type, 'colophon') or 77 | contains(@data-type, 'conclusion') or 78 | contains(@data-type, 'copyright-page') or 79 | contains(@data-type, 'dedication') or 80 | contains(@data-type, 'foreword') or 81 | contains(@data-type, 'glossary') or 82 | contains(@data-type, 'halftitlepage') or 83 | contains(@data-type, 'index') or 84 | contains(@data-type, 'introduction') or 85 | contains(@data-type, 'preface') or 86 | contains(@data-type, 'titlepage') or 87 | contains(@data-type, 'toc')]| 88 | ancestor::h:div[@data-type = 'part'])[last()]" mode="label.markup"/> 89 | </xsl:otherwise> 90 | </xsl:choose> 91 | </xsl:template> 92 | 93 | </xsl:stylesheet> 94 | -------------------------------------------------------------------------------- /titlepage.html: -------------------------------------------------------------------------------- 1 | <section data-type="titlepage" xmlns="http://www.w3.org/1999/xhtml"> 2 | <h1>Architecture Patterns with Python</h1> 3 | <!--(only include edition line if it's 2e or higher --> 4 | 5 | <p class="subtitle">Enabling Test-Driven Development, <span class="keep-together">Domain-Driven Design</span>, and <span class="keep-together">Event-Driven Microservices</span></p> 6 | 7 | <p class="author">Harry Percival and Bob Gregory</p> 8 | </section> 9 | <!-- if a pocket ref, include this line below the h1: 10 | <p data-type="subtitle">Pocket Reference/Guide</p> --> 11 | -------------------------------------------------------------------------------- /toc.html: -------------------------------------------------------------------------------- 1 | <!-- This is a placeholder element for use with the automatic TOC generation option in Atlas --> 2 | <nav data-type="toc" xmlns="http://www.w3.org/1999/xhtml"/> 3 | -------------------------------------------------------------------------------- /tools/figure_renaming_report.tsv: -------------------------------------------------------------------------------- 1 | Original names New names 2 | images/big_ball_of_yarn.jpg images/apwp_0001.png 3 | images/layered_architecture.png images/apwp_0002.png 4 | images/part1_components_diagram.png images/apwp_p101.png 5 | images/maps_chapter_01_notext.png images/apwp_0101.png 6 | images/allocation_context_diagram.png images/apwp_0102.png 7 | images/model_diagram.png images/apwp_0103.png 8 | images/maps_chapter_01_withtext.png images/apwp_0104.png 9 | images/maps_chapter_02.png images/apwp_0201.png 10 | images/layered_architecture.png images/apwp_0202.png 11 | images/onion_architecture.png images/apwp_0203.png 12 | images/model_diagram.png images/apwp_0204.png 13 | images/repository_pattern_diagram.png images/apwp_0205.png 14 | images/domain_model_tradeoffs_diagram.png images/apwp_0206.png 15 | images/coupling_illustration1.png images/apwp_0301.png 16 | images/coupling_illustration2.png images/apwp_0302.png 17 | images/maps_chapter_04_before.png images/apwp_0401.png 18 | images/maps_chapter_04_after.png images/apwp_0402.png 19 | images/service_layer_diagram_abstract_dependencies.png images/apwp_0403.png 20 | images/service_layer_diagram_test_dependencies.png images/apwp_0404.png 21 | images/service_layer_diagram_runtime_dependencies.png images/apwp_0405.png 22 | images/test_spectrum_diagram.png images/apwp_0501.png 23 | images/maps_chapter_05_before.png images/apwp_0601.png 24 | images/maps_chapter_05_after.png images/apwp_0602.png 25 | images/maps_chapter_06.png images/apwp_0701.png 26 | images/before_aggregates_diagram.png images/apwp_0702.png 27 | images/after_aggregates_diagram.png images/apwp_0703.png 28 | images/version_numbers_sequence_diagram.png images/apwp_0704.png 29 | images/allocation_context_diagram.png images/apwp_p201.png 30 | images/maps_chapter_07.png images/apwp_0801.png 31 | images/maps_chapter_08_before.png images/apwp_0901.png 32 | images/maps_chapter_08_after.png images/apwp_0902.png 33 | images/batch_changed_events_flow_diagram.png images/apwp_0903.png 34 | images/reallocation_sequence_diagram.png images/apwp_0904.png 35 | images/maps_chapter_10.png images/apwp_1101.png 36 | images/batches_context_diagram.png images/apwp_1102.png 37 | images/command_flow_diagram_1.png images/apwp_1103.png 38 | images/command_flow_diagram_2.png images/apwp_1104.png 39 | images/command_flow_diagram_with_error.png images/apwp_1105.png 40 | images/reallocation_sequence_diagram_with_redis.png images/apwp_1106.png 41 | images/maps_chapter_11.png images/apwp_1201.png 42 | images/read_model_sequence_diagram.png images/apwp_1202.png 43 | images/maps_chapter_12_before.png images/apwp_1301.png 44 | images/maps_chapter_12_after.png images/apwp_1302.png 45 | images/maps_chapter_12_after.png images/apwp_aa01.png 46 | -------------------------------------------------------------------------------- /tools/intakereport.txt: -------------------------------------------------------------------------------- 1 | Title: Architecture Patterns with Python 2 | ISBN: 9781492052203 3 | JIRA #: DCPSPROD-3883 4 | 5 | 6 | Stylesheet: animal_theme_sass 7 | Toolchain: Atlas 2 8 | 9 | Atlas URL: https://atlas.oreilly.com/oreillymedia/architecture-patterns-with-python 10 | 11 | Incoming format: Asciidoc 12 | Outgoing format: Asciidoc 13 | 14 | Preliminary pagecount: 292 15 | 16 | Is this project in Early Release? YES 17 | 18 | Resources 19 | ========= 20 | 21 | ** Figs: Illustrations is still working on the figs. 22 | 44 total. (0 are informal; 465 are inline.) 23 | 24 | Once the figs are processed on /work, you'll need to add them to the book's repo. 25 | 26 | A report mapping original figure file names to their new names can be found in the tools folder for this project as figure_renaming_report.tsv. 27 | 28 | ** Intake Report: 29 | (Git repo) tools/intakereport.txt 30 | 31 | ** MS Snapshot: 32 | To view the submitted files, you'll need to checkout the git tag named 'manuscript_to_prod' 33 | by running the following command in your checkout: 34 | 35 | $ git checkout manuscript_to_prod 36 | 37 | This will temporarily switch the files in your repo to the state they were in when the manuscript_to_prod tag 38 | was created. 39 | To switch the files back to the current state, run: 40 | 41 | $ git checkout master 42 | 43 | 44 | Notes from Tools: 45 | ================= 46 | 47 | ** PROD: Add any authors to project that need to be added. 48 | ** Syntax highlighting: applied to 218 out of 264 code listings. 49 | 50 | ** Note that the URL of the repo has been changed to match the title. 51 | ** The errors related to code callouts stem from two callouts that are missing their corresponding list items. These are located at chapter_13_dependency_injection.asciidoc line 689 and appendix_validation.asciidoc line 405. Please have the author advise on whether to remove the callout or add the missing list item. 52 | ** As noted below, there are some customizations I made to the Examples. Please see TOOLSREQ-4934 for details. 53 | 54 | ** Please let Tools know ASAP if there are any other global problems for which we can help automate a fix. 55 | 56 | 57 | Notes at arrival in production: 58 | ================== 59 | 60 | Notes for Production: 61 | * They are hoping to have print copies to sign at the Pycon Conference April 15-23, 2020. That's a little before the ERD, so if extra effort can be made to let this happen, it would be great. [KB added padding to the schedule to account for your honeymoon, and we have an early March FTP. If we can hold that and make this a March ERD instead of April ERD, big hurrah!] 62 | * Both authors live in the UK, so you may experience delays in communication. Harry functions as the lead and responds far more often than Bob. Confirm with them, but usually an okay from Harry is good enough for approval. Make sure he agrees and knows to rouse Bob on anything he wants Bob to specifically approve. [KB has worked with Harry before, he's super nice!] 63 | * Figure log is at https://docs.google.com/spreadsheets/d/1xg0EhcmcSklv9WxqyFbB0Kvcsj9lPGxr/edit#gid=29876908. 64 | * All figures can be used as-is as far as we can tell. No need for redraws. [KB sez: these do look fine, though slight inconsistencies in style. Some of them have light blue text that may have legibility issues, so going to ask Rebecca to darken them.] 65 | * Authors using Examples but want the "Example X-X" bits hidden. Tools has modified the asciidoc for this project to remove the "Example X-X" from appearing, so maybe production doesn't have to do anything on this. They also have the example titles right-aligned, which is intentional (you can ask KB about Harry Percival's other book and the formatting quirks, if you're curious). 66 | * Watch for use of singular "I" – author voice should be plural "we" 67 | * Watch for sentence style headings; use ORM heading styles 68 | * Take a light hand in the edit, unless stuff is just wrong. If in doubt, best to query. They have some quirky ideas about writing, and I gave them more than the usual leeway. They needed some "wins" since we imposed the book's title against their will and really put the hammer down in terms of schedule. They are also proud of UK English and may try to sneak some in, including using pounds instead of dollars. I recommend letting a lot of that go if it seems insignificant. 69 | * Tools: in epubcheck, I see some errors related to callout list icons. [Nick: See Notes from Tools] 70 | * Some duplicate hardcoded part numbers (Part I: Part 1: Title) [Nick: I fixed the Part titles] 71 | 72 | ================== 73 | 74 | Please let me know about any other issues. 75 | 76 | Thanks, 77 | -Nick 78 | 79 | -------------------------------------------------------------------------------- /travis-deploy-key.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicpython/book/7e10e79c1db961ea071daf57c4aed3089d9b7ed2/travis-deploy-key.enc -------------------------------------------------------------------------------- /update-exercise-branch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | import subprocess 4 | from pathlib import Path 5 | 6 | 7 | def run(cmds): 8 | print(' '.join(cmds)) 9 | p = subprocess.run( 10 | cmds, 11 | cwd=Path(__file__).parent / 'code', 12 | capture_output=True, 13 | check=True 14 | ) 15 | if p.returncode: 16 | raise Exception(p.stderr.decode()) 17 | output = p.stdout.decode() 18 | print(output) 19 | return output 20 | 21 | all_branches = run(['git', 'branch', '-a'],) 22 | 23 | 24 | def main(chapter): 25 | exercise_chapter = f'{chapter}_exercise' 26 | assert exercise_chapter in all_branches 27 | 28 | run(['git', 'checkout', exercise_chapter]) 29 | commits = list(reversed(run([ 30 | 'git', 'log', '--pretty=%h', 31 | f'{exercise_chapter}^{{/{chapter}_ends}}..{exercise_chapter}', 32 | ]).split())) 33 | run(['git', 'reset', '--hard', chapter]) 34 | run(['git', 'cherry-pick', *commits]) 35 | run(['git', 'checkout', 'master']) 36 | 37 | if __name__ == '__main__': 38 | main(sys.argv[1]) 39 | -------------------------------------------------------------------------------- /uppercase-titles.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import re 3 | from pathlib import Path 4 | from titlecase import titlecase 5 | from chapters import CHAPTERS 6 | 7 | SPECIAL = { 8 | 'CSVs', 'To/From', 'UoW', 'AKA', 'SELECT', 9 | } 10 | 11 | LOWERCASE = { 12 | 'with', 13 | } 14 | 15 | 16 | 17 | def specialcases(w, **_): 18 | if w.lower() in LOWERCASE: 19 | return w.lower() 20 | if w.lower() in {s.lower() for s in SPECIAL}: 21 | return next(special for special in SPECIAL if special.lower() == w.lower()) 22 | if '_' in w: 23 | return w 24 | return None 25 | 26 | 27 | def fix_line(l): 28 | if not l.startswith('=='): 29 | return l 30 | if l == '====': 31 | return l 32 | if l == '.': 33 | return l 34 | prefix, rest = re.match(r'(=+ |\.)(.+)', l).groups() 35 | return prefix + titlecase(rest, callback=specialcases) 36 | 37 | 38 | def main(): 39 | for chapter in CHAPTERS: 40 | path = Path(f'{chapter}.asciidoc') 41 | contents = path.read_text() 42 | fixed = '\n'.join( 43 | fix_line(l) for l in contents.splitlines() 44 | ) 45 | path.write_text(fixed) 46 | 47 | if __name__ == '__main__': 48 | main() 49 | 50 | import pytest 51 | 52 | def test_lowercases_short_words(): 53 | assert fix_line('=== What Is A Domain Model') == '=== What Is a Domain Model' 54 | 55 | def test_fix_line_handles_quotes_and_slashes(): 56 | assert fix_line('=== Foo "bar" baz') == '=== Foo "Bar" Baz' 57 | 58 | def test_fix_line_leaves_small_words_alone_except_at_beginning(): 59 | assert fix_line('=== This is a line') == '=== This Is a Line' 60 | assert fix_line('=== The initial the is fine') == '=== The Initial the Is Fine' 61 | 62 | @pytest.mark.skip 63 | def test_dotstarters(): 64 | assert fix_line('.A sidebar title') == '.A Sidebar Title' 65 | 66 | def test_hyphens(): 67 | assert fix_line('=== A wrap-up') == '=== A Wrap-Up' 68 | 69 | def test_uow(): 70 | assert fix_line('=== A Uow') == '=== A UoW' 71 | 72 | def test_underscores(): 73 | assert fix_line('=== A special_method') == '=== A special_method' 74 | --------------------------------------------------------------------------------