├── .gitignore ├── .readthedocs.yaml ├── LICENSE ├── Makefile ├── README.md ├── ddd ├── __init__.py ├── adapters │ ├── __init__.py │ ├── adapter.py │ ├── event │ │ ├── __init__.py │ │ ├── azure │ │ │ ├── __init__.py │ │ │ └── azure_event_adapter.py │ │ ├── event_adapter.py │ │ ├── event_listener.py │ │ ├── kafka │ │ │ ├── __init__.py │ │ │ └── kafka_event_adapter.py │ │ └── memory │ │ │ ├── __init__.py │ │ │ └── memory_event_adapter.py │ ├── http │ │ ├── __init__.py │ │ ├── exceptions.py │ │ └── http_adapter.py │ ├── job │ │ ├── __init__.py │ │ ├── ap_scheduler_adapter.py │ │ ├── exceptions.py │ │ ├── job_adapter.py │ │ ├── memory_scheduler_adapter.py │ │ └── scheduler_adapter.py │ └── message_reader.py ├── application │ ├── __init__.py │ ├── action.py │ ├── application_service.py │ ├── config.py │ ├── domain_registry.py │ └── dummy_application_service.py ├── domain │ ├── __init__.py │ ├── aggregate.py │ ├── azure │ │ ├── __init__.py │ │ └── azure_event_publisher.py │ ├── building_block.py │ ├── command.py │ ├── dummy │ │ ├── __init__.py │ │ ├── dummy.py │ │ ├── dummy_id.py │ │ ├── dummy_repository.py │ │ └── dummy_translator.py │ ├── entity.py │ ├── entity_id.py │ ├── entity_id_translator.py │ ├── event │ │ ├── __init__.py │ │ ├── domain_event.py │ │ ├── event.py │ │ └── integration_event.py │ ├── event_publisher.py │ ├── exceptions.py │ ├── kafka │ │ ├── __init__.py │ │ └── kafka_event_publisher.py │ ├── memory │ │ ├── __init__.py │ │ └── memory_event_publisher.py │ ├── service.py │ └── value_object.py ├── infrastructure │ ├── __init__.py │ ├── container.py │ ├── db_service │ │ ├── __init__.py │ │ ├── db_service.py │ │ ├── memory_db_service.py │ │ ├── memory_pool.py │ │ ├── memory_postgres_db_service.py │ │ └── postgres_db_service.py │ ├── email_service.py │ ├── infrastructure_service.py │ ├── job_service.py │ └── log_service.py ├── repositories │ ├── __init__.py │ ├── memory │ │ ├── __init__.py │ │ ├── memory_dummy_repository.py │ │ ├── memory_event_repository.py │ │ └── memory_repository.py │ ├── postgres │ │ ├── __init__.py │ │ ├── postgres_dummy_repository.py │ │ └── postgres_repository.py │ └── repository.py ├── tests │ ├── __init__.py │ ├── action_test_case.py │ ├── adapters │ │ ├── __init__.py │ │ └── event │ │ │ ├── __init__.py │ │ │ └── test_event_adapter.py │ ├── base_test_case.py │ ├── dummy_action_test_case.py │ ├── repositories │ │ ├── __init__.py │ │ ├── memory │ │ │ ├── __init__.py │ │ │ └── test_memory_repository.py │ │ ├── postgres │ │ │ ├── __init__.py │ │ │ └── test_postgres_repository.py │ │ └── test_repository.py │ └── utils │ │ ├── __init__.py │ │ └── tasks │ │ ├── __init__.py │ │ └── test_migrate_models.py └── utils │ ├── __init__.py │ ├── dep_mgr.py │ ├── tasks │ ├── __init__.py │ ├── migrate_models.py │ └── task.py │ └── utils.py ├── development ├── Makefile ├── env.sample └── postgres │ └── postgresql.conf ├── docs ├── Makefile ├── _build │ ├── doctrees │ │ ├── community.doctree │ │ ├── environment.pickle │ │ ├── gettingstarted.doctree │ │ ├── index.doctree │ │ ├── modules │ │ │ ├── adapters │ │ │ │ └── adapter.doctree │ │ │ ├── application │ │ │ │ ├── action.doctree │ │ │ │ ├── application_service.doctree │ │ │ │ └── config.doctree │ │ │ ├── domain │ │ │ │ └── event │ │ │ │ │ ├── domain_event.doctree │ │ │ │ │ └── integration_event.doctree │ │ │ ├── infrastructure │ │ │ │ └── db_service │ │ │ │ │ ├── db_service.doctree │ │ │ │ │ ├── memory_db_service.doctree │ │ │ │ │ ├── memory_postgres_db_service.doctree │ │ │ │ │ └── postgres_db_service.doctree │ │ │ └── utils │ │ │ │ ├── dep_mgr │ │ │ │ └── dep_mgr.doctree │ │ │ │ └── tasks │ │ │ │ └── task │ │ │ │ └── task.doctree │ │ ├── py-modindex.doctree │ │ └── versionhistory.doctree │ └── html │ │ ├── .buildinfo │ │ ├── .doctrees │ │ ├── community.doctree │ │ ├── environment.pickle │ │ ├── gettingstarted.doctree │ │ ├── index.doctree │ │ ├── modules │ │ │ ├── adapters │ │ │ │ └── adapter.doctree │ │ │ ├── application │ │ │ │ ├── action.doctree │ │ │ │ ├── application_service.doctree │ │ │ │ └── config.doctree │ │ │ ├── domain │ │ │ │ └── event │ │ │ │ │ ├── domain_event.doctree │ │ │ │ │ └── integration_event.doctree │ │ │ ├── infrastructure │ │ │ │ └── db_service │ │ │ │ │ ├── db_service.doctree │ │ │ │ │ ├── memory_db_service.doctree │ │ │ │ │ ├── memory_postgres_db_service.doctree │ │ │ │ │ └── postgres_db_service.doctree │ │ │ └── utils │ │ │ │ ├── dep_mgr │ │ │ │ └── dep_mgr.doctree │ │ │ │ └── tasks │ │ │ │ └── task │ │ │ │ └── task.doctree │ │ ├── py-modindex.doctree │ │ └── versionhistory.doctree │ │ ├── _sources │ │ ├── community.rst.txt │ │ ├── gettingstarted.rst.txt │ │ ├── index.rst.txt │ │ ├── modules │ │ │ ├── adapters │ │ │ │ └── adapter.rst.txt │ │ │ ├── application │ │ │ │ ├── action.rst.txt │ │ │ │ ├── application_service.rst.txt │ │ │ │ └── config.rst.txt │ │ │ ├── domain │ │ │ │ └── event │ │ │ │ │ ├── domain_event.rst.txt │ │ │ │ │ └── integration_event.rst.txt │ │ │ ├── infrastructure │ │ │ │ └── db_service │ │ │ │ │ ├── db_service.rst.txt │ │ │ │ │ ├── memory_db_service.rst.txt │ │ │ │ │ ├── memory_postgres_db_service.rst.txt │ │ │ │ │ └── postgres_db_service.rst.txt │ │ │ └── utils │ │ │ │ ├── dep_mgr │ │ │ │ └── dep_mgr.rst.txt │ │ │ │ └── tasks │ │ │ │ └── task │ │ │ │ └── task.rst.txt │ │ ├── py-modindex.rst.txt │ │ └── versionhistory.rst.txt │ │ ├── _static │ │ ├── basic.css │ │ ├── css │ │ │ ├── badge_only.css │ │ │ ├── fonts │ │ │ │ ├── Roboto-Slab-Bold.woff │ │ │ │ ├── Roboto-Slab-Bold.woff2 │ │ │ │ ├── Roboto-Slab-Regular.woff │ │ │ │ ├── Roboto-Slab-Regular.woff2 │ │ │ │ ├── fontawesome-webfont.eot │ │ │ │ ├── fontawesome-webfont.svg │ │ │ │ ├── fontawesome-webfont.ttf │ │ │ │ ├── fontawesome-webfont.woff │ │ │ │ ├── fontawesome-webfont.woff2 │ │ │ │ ├── lato-bold-italic.woff │ │ │ │ ├── lato-bold-italic.woff2 │ │ │ │ ├── lato-bold.woff │ │ │ │ ├── lato-bold.woff2 │ │ │ │ ├── lato-normal-italic.woff │ │ │ │ ├── lato-normal-italic.woff2 │ │ │ │ ├── lato-normal.woff │ │ │ │ └── lato-normal.woff2 │ │ │ └── theme.css │ │ ├── doctools.js │ │ ├── documentation_options.js │ │ ├── file.png │ │ ├── jquery-3.5.1.js │ │ ├── jquery.js │ │ ├── js │ │ │ ├── badge_only.js │ │ │ ├── html5shiv-printshiv.min.js │ │ │ ├── html5shiv.min.js │ │ │ └── theme.js │ │ ├── language_data.js │ │ ├── minus.png │ │ ├── plus.png │ │ ├── pygments.css │ │ ├── searchtools.js │ │ ├── underscore-1.13.1.js │ │ └── underscore.js │ │ ├── community.html │ │ ├── genindex.html │ │ ├── gettingstarted.html │ │ ├── index.html │ │ ├── modules │ │ ├── adapters │ │ │ └── adapter.html │ │ ├── application │ │ │ ├── action.html │ │ │ ├── application_service.html │ │ │ └── config.html │ │ ├── domain │ │ │ └── event │ │ │ │ ├── domain_event.html │ │ │ │ └── integration_event.html │ │ ├── infrastructure │ │ │ └── db_service │ │ │ │ ├── db_service.html │ │ │ │ ├── memory_db_service.html │ │ │ │ ├── memory_postgres_db_service.html │ │ │ │ └── postgres_db_service.html │ │ └── utils │ │ │ ├── dep_mgr │ │ │ └── dep_mgr.html │ │ │ └── tasks │ │ │ └── task │ │ │ └── task.html │ │ ├── objects.inv │ │ ├── py-modindex.html │ │ ├── search.html │ │ ├── searchindex.js │ │ └── versionhistory.html ├── community.rst ├── conf.py ├── gettingstarted.rst ├── images │ └── folderstructure.png ├── index.rst ├── make.bat ├── modules │ ├── adapters │ │ └── adapter.rst │ ├── application │ │ ├── action.rst │ │ ├── application_service.rst │ │ └── config.rst │ ├── domain │ │ └── event │ │ │ ├── domain_event.rst │ │ │ └── integration_event.rst │ ├── infrastructure │ │ └── db_service │ │ │ ├── db_service.rst │ │ │ ├── memory_db_service.rst │ │ │ ├── memory_postgres_db_service.rst │ │ │ └── postgres_db_service.rst │ └── utils │ │ ├── dep_mgr │ │ └── dep_mgr.rst │ │ └── tasks │ │ └── task │ │ └── task.rst ├── py-modindex.rst └── versionhistory.rst ├── env.sample ├── env.test ├── examples └── webshop │ ├── README.md │ └── shipping │ ├── .gitignore │ ├── Makefile │ ├── README.md │ ├── env.sample │ ├── env.test │ ├── env.test_pipeline │ ├── requirements.txt │ └── src │ ├── main.py │ ├── run_task.py │ └── shipping │ ├── __init__.py │ ├── adapters │ ├── __init__.py │ ├── http │ │ └── __init__.py │ └── listeners │ │ ├── __init__.py │ │ └── domain │ │ ├── __init__.py │ │ └── shipment_created_listener.py │ ├── application │ ├── __init__.py │ ├── config.py │ └── shipping_application_service.py │ ├── domain │ ├── __init__.py │ ├── commands.py │ ├── customer │ │ ├── __init__.py │ │ ├── customer.py │ │ ├── customer_id.py │ │ ├── customer_repository.py │ │ └── customer_translator.py │ └── shipment │ │ ├── __init__.py │ │ ├── shipment.py │ │ ├── shipment_created.py │ │ ├── shipment_id.py │ │ ├── shipment_repository.py │ │ └── shipment_translator.py │ ├── repositories │ ├── __init__.py │ ├── memory │ │ ├── __init__.py │ │ ├── memory_customer_repository.py │ │ └── memory_shipment_repository.py │ └── postgres │ │ ├── __init__.py │ │ ├── postgres_customer_repository.py │ │ └── postgres_shipment_repository.py │ ├── tests │ ├── __init__.py │ ├── domain │ │ ├── __init__.py │ │ └── actions │ │ │ └── __init__.py │ └── shipping_action_test_case.py │ └── utils │ ├── __init__.py │ ├── dep_mgr.py │ ├── tasks │ ├── __init__.py │ └── setup_dev_data.py │ └── utils.py ├── requirements.txt ├── setup.py └── templates └── project ├── README.md └── domain ├── Makefile ├── README.md ├── env.sample ├── env.test ├── env.test_pipeline ├── requirements.txt └── src ├── domain ├── __init__.py ├── adapters │ ├── __init__.py │ ├── http │ │ └── __init__.py │ └── listeners │ │ ├── __init__.py │ │ ├── domain │ │ └── __init__.py │ │ └── interchange │ │ └── __init__.py ├── application │ ├── __init__.py │ ├── config.py │ └── domain_application_service.py ├── domain │ ├── __init__.py │ ├── commands.py │ └── foo │ │ ├── __init__.py │ │ ├── foo.py │ │ ├── foo_id.py │ │ ├── foo_repository.py │ │ └── foo_translator.py ├── repositories │ ├── __init__.py │ ├── memory │ │ ├── __init__.py │ │ └── memory_foo_repository.py │ └── postgres │ │ ├── __init__.py │ │ └── postgres_foo_repository.py ├── tests │ ├── __init__.py │ ├── integration │ │ └── __init__.py │ └── unit │ │ ├── __init__.py │ │ └── application │ │ ├── __init__.py │ │ └── actions │ │ └── __init__.py └── utils │ ├── __init__.py │ ├── dep_mgr.py │ ├── tasks │ ├── __init__.py │ └── seed_local_env.py │ └── utils.py ├── main.py └── run_task.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | env 4 | 5 | venv/ 6 | 7 | development/postgres/data 8 | 9 | build 10 | dist 11 | *.egg-info 12 | 13 | __pycache__/ 14 | *.7z 15 | *.zip 16 | 17 | scratch 18 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | formats: [htmlzip, pdf] 4 | 5 | sphinx: 6 | configuration: docs/conf.py 7 | 8 | python: 9 | version: 3.8 10 | install: 11 | - requirements: requirements.txt 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ########################################################################## 2 | # This is the project's Makefile. 3 | ########################################################################## 4 | 5 | ########################################################################## 6 | # VARIABLES 7 | ########################################################################## 8 | 9 | HOME := $(shell echo ~) 10 | PWD := $(shell pwd) 11 | ENV := ENV_FILE=env 12 | ENV_TEST := ENV_FILE=env.test 13 | PYTHON := venv/bin/python 14 | 15 | ########################################################################## 16 | # MENU 17 | ########################################################################## 18 | 19 | .PHONY: help 20 | help: 21 | @awk 'BEGIN {FS = ":.*?## "} /^[0-9a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) 22 | 23 | ########################################################################## 24 | # TEST 25 | ########################################################################## 26 | 27 | .PHONY: test 28 | test: ## run test suite 29 | $(ENV_TEST) $(PYTHON) -m unittest discover ./ddd 30 | 31 | ################################################################################ 32 | # RELEASE 33 | ################################################################################ 34 | 35 | .PHONY: build 36 | build: ## build the python package 37 | python setup.py sdist bdist_wheel 38 | 39 | .PHONY: clean 40 | clean: ## clean the build 41 | python setup.py clean 42 | rm -rf build dist ddd_for_python.egg-info 43 | find . -type f -name '*.py[co]' -delete -o -type d -name __pycache__ -delete 44 | 45 | .PHONY: upload-test 46 | upload-test: ## upload package to testpypi repository 47 | twine upload --repository testpypi --skip-existing --repository-url https://test.pypi.org/legacy/ dist/* 48 | 49 | .PHONY: upload 50 | upload: ## upload package to pypi repository 51 | twine upload --skip-existing dist/* 52 | 53 | .PHONY: sphinx-quickstart 54 | sphinx-quickstart: ## run the sphinx quickstart 55 | docker run -it --rm -v $(PWD)/docs:/docs sphinxdoc/sphinx sphinx-quickstart 56 | 57 | .PHONY: sphinx-html 58 | sphinx-html: ## build the sphinx html 59 | make -C docs html 60 | 61 | .PHONY: sphinx-rebuild 62 | sphinx-rebuild: ## re-build the sphinx docs 63 | make -C docs clean && make -C docs html 64 | 65 | .PHONY: sphinx-autobuild 66 | sphinx-autobuild: ## activate autobuild of docs 67 | sphinx-autobuild docs docs/_build/html --watch ddd 68 | 69 | .PHONY: install-requirements 70 | install-requirements: ## install requirements 71 | pip install -r requirements.txt 72 | -------------------------------------------------------------------------------- /ddd/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/ddd/__init__.py -------------------------------------------------------------------------------- /ddd/adapters/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/ddd/adapters/__init__.py -------------------------------------------------------------------------------- /ddd/adapters/adapter.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | 3 | 4 | class Adapter(object): 5 | """ 6 | The adapters base class. 7 | 8 | :param config: The :class:`~ddd.application.config.Config` object. 9 | :param loop: The event loop. 10 | :param log_service: The :class:`~ddd.infrastructure.log_service.LogService` object. 11 | """ 12 | def __init__( 13 | config, 14 | loop, 15 | log_service 16 | ): 17 | super().__init__() 18 | 19 | # Dependencies 20 | self.config = config 21 | self.loop = loop 22 | self.log_service = log_service 23 | 24 | # Abstract methods (must be implemented by superclasses) 25 | 26 | @abstractmethod 27 | async def start(self): 28 | """ 29 | Starts the adapter. 30 | 31 | This method is called by the :class:`~ddd.application.application_service.ApplicationService`. When it starts, it starts all the adapters. 32 | """ 33 | pass 34 | 35 | @abstractmethod 36 | async def stop(self): 37 | """ 38 | Stops the adapter. 39 | 40 | This method is called by the :class:`~ddd.application.application_service.ApplicationService`. When it stops, it stops all the adapters. 41 | """ 42 | pass 43 | 44 | # Service 45 | 46 | def set_service(self, service): 47 | self.service = service 48 | -------------------------------------------------------------------------------- /ddd/adapters/event/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/ddd/adapters/event/__init__.py -------------------------------------------------------------------------------- /ddd/adapters/event/azure/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/ddd/adapters/event/azure/__init__.py -------------------------------------------------------------------------------- /ddd/adapters/event/azure/azure_event_adapter.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | 4 | from azure.eventhub.aio import EventHubConsumerClient 5 | from azure.eventhub.extensions.checkpointstoreblobaio import \ 6 | BlobCheckpointStore 7 | 8 | from ddd.adapters.event.event_adapter import EventAdapter 9 | 10 | 11 | class AzureEventAdapter(EventAdapter): 12 | 13 | def __init__( 14 | self, 15 | namespace_conn_string, 16 | checkpoint_store_conn_string, 17 | blob_container_name, 18 | topic, 19 | group, 20 | log_service, 21 | service=None, 22 | listeners=None, 23 | ): 24 | super().__init__( 25 | log_service=log_service, 26 | service=service, 27 | listeners=listeners 28 | ) 29 | 30 | self.namespace_conn_string = namespace_conn_string 31 | self.checkpoint_store_conn_string = checkpoint_store_conn_string 32 | self.blob_container_name = blob_container_name 33 | self.topic = topic 34 | self.group = group 35 | 36 | self.checkpoint_store = None 37 | self.consumer = None 38 | self.consumer_task = None 39 | 40 | # Handling 41 | 42 | async def handle(self, message): 43 | """ 44 | Handle a message. 45 | """ 46 | message = json.loads(message.body_as_str(encoding='UTF-8')) 47 | await super().handle(message) 48 | 49 | # Control 50 | 51 | async def start(self): 52 | await self._create_checkpoint_store() 53 | await self._create_consumer() 54 | await self._start_consuming() 55 | 56 | async def stop(self): 57 | await self._stop_consuming() 58 | 59 | # Azure 60 | 61 | async def _create_checkpoint_store(self): 62 | self.checkpoint_store = \ 63 | BlobCheckpointStore.from_connection_string( 64 | self.checkpoint_store_conn_string, 65 | self.blob_container_name, 66 | ) 67 | 68 | async def _create_consumer(self): 69 | self.consumer = \ 70 | EventHubConsumerClient.from_connection_string( 71 | self.namespace_conn_string, 72 | consumer_group=self.group, 73 | eventhub_name=self.topic, 74 | checkpoint_store=self.checkpoint_store, 75 | ) 76 | 77 | async def _start_consuming(self): 78 | self.consumer_task = \ 79 | asyncio.ensure_future( 80 | self.consumer.receive( 81 | on_event=self._on_event, 82 | ) 83 | ) 84 | await asyncio.sleep(0.01) 85 | 86 | async def _on_event(self, partition_context, event): 87 | try: 88 | await self.handle(event) 89 | except Exception as e: 90 | self.log_service.error( 91 | f"Azure event adapter got exception when " 92 | f"delegating call to service: '{str(e)}'.", 93 | exc_info=True, 94 | ) 95 | 96 | # Update the checkpoint so that the program doesn't read the events 97 | # that it has already read when you run it next time. 98 | await partition_context.update_checkpoint(event) 99 | 100 | async def _stop_consuming(self): 101 | self.consumer_task.cancel() 102 | await self.consumer.close() 103 | -------------------------------------------------------------------------------- /ddd/adapters/event/event_adapter.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | 3 | from ddd.application.domain_registry import DomainRegistry 4 | 5 | 6 | class EventAdapter(object, metaclass=ABCMeta): 7 | 8 | def __init__(self, log_service=None, service=None, listeners=None): 9 | super().__init__() 10 | 11 | self.log_service = \ 12 | log_service \ 13 | if log_service \ 14 | else DomainRegistry.get_instance().log_service 15 | 16 | self.service = service 17 | self.listeners = [] if listeners is None else [] 18 | self._assign_service_to_listeners() 19 | 20 | # Service 21 | 22 | def set_service(self, service): 23 | self.service = service 24 | self._assign_service_to_listeners() 25 | 26 | # Listeners 27 | 28 | def set_listeners(self, listeners): 29 | self.listeners = listeners 30 | self._assign_service_to_listeners() 31 | 32 | # Handling 33 | 34 | async def handle(self, message): 35 | for listener in self.listeners: 36 | if listener.listens_to(message['name']): 37 | await listener.handle(message) 38 | 39 | # Control 40 | 41 | @abstractmethod 42 | async def start(self): 43 | pass 44 | 45 | @abstractmethod 46 | async def stop(self): 47 | pass 48 | 49 | # Helpers 50 | 51 | def _assign_service_to_listeners(self): 52 | for listener in self.listeners: 53 | listener.service = self.service 54 | -------------------------------------------------------------------------------- /ddd/adapters/event/event_listener.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta 2 | 3 | from ddd.adapters.message_reader import MessageReader 4 | 5 | 6 | class EventListener(object, metaclass=ABCMeta): 7 | """ 8 | An event listener. 9 | """ 10 | def __init__( 11 | self, 12 | listens_to, 13 | action, 14 | command_creator, 15 | service=None, 16 | enabled=True, 17 | ): 18 | super(EventListener, self).__init__() 19 | 20 | self.listens_to_name = listens_to 21 | self.action = action 22 | self.service = service 23 | self.command_creator = command_creator 24 | self.enabled = enabled 25 | 26 | def listens_to(self, name): 27 | """ 28 | Used to check if we listen to an event named 'name'. 29 | """ 30 | if not self.listens_to_name: 31 | raise Exception("The 'listens_to_name' variable hasn't been set.") 32 | return name == self.listens_to_name 33 | 34 | async def handle(self, message): 35 | """ 36 | Called when an event we are listening to is received. 37 | """ 38 | if not self.enabled: 39 | return 40 | 41 | if self.command_creator is None: 42 | raise Exception( 43 | "Can't delegate event because command_creator is null.") 44 | 45 | self.message = message 46 | self.event = self.read_event() 47 | 48 | try: 49 | command = \ 50 | self.command_creator( 51 | event=self.event 52 | ) 53 | except Exception as e: 54 | raise Exception( 55 | f"Couldn't create command using command " 56 | f"creator, error: {str(e)}" 57 | ) 58 | action = getattr(self.service, self.action) 59 | 60 | await action( 61 | command=command, 62 | corr_ids=self.event.corr_ids, 63 | ) 64 | 65 | def read_event(self): 66 | """ 67 | Reads basic information of the event. 68 | """ 69 | # Create reader 70 | self.reader = MessageReader(self.message) 71 | 72 | # Common properties 73 | self.type = self.reader.string_value('type') 74 | self.version = self.reader.string_value('version') 75 | self.name = self.reader.string_value('name') 76 | self.action_id = self.reader.string_value('action_id') 77 | self.corr_ids = self.reader.list_value('corr_ids') 78 | 79 | def enable(self): 80 | self.enabled = True 81 | 82 | def disable(self): 83 | self.enabled = False 84 | -------------------------------------------------------------------------------- /ddd/adapters/event/kafka/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/ddd/adapters/event/kafka/__init__.py -------------------------------------------------------------------------------- /ddd/adapters/event/memory/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/ddd/adapters/event/memory/__init__.py -------------------------------------------------------------------------------- /ddd/adapters/event/memory/memory_event_adapter.py: -------------------------------------------------------------------------------- 1 | from ddd.adapters.event.event_adapter import EventAdapter 2 | 3 | 4 | class MemoryEventAdapter(EventAdapter): 5 | 6 | def __init__( 7 | self, 8 | log_service=None, 9 | service=None, 10 | listeners=None, 11 | ): 12 | super().__init__( 13 | log_service=log_service, 14 | service=service, 15 | listeners=listeners 16 | ) 17 | 18 | # Control 19 | 20 | async def start(self): 21 | pass 22 | 23 | async def stop(self): 24 | pass 25 | -------------------------------------------------------------------------------- /ddd/adapters/http/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/ddd/adapters/http/__init__.py -------------------------------------------------------------------------------- /ddd/adapters/http/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | The http adapter exceptions. 3 | """ 4 | class HttpException(Exception): 5 | """ 6 | Base class for http adapter exceptions. 7 | This class must not be used directly. Throw one of it's subclasses instead. 8 | """ 9 | def __init__(self, exception): 10 | super(HttpException, self).__init__() 11 | self.exception = exception 12 | 13 | 14 | class NotReachableException(HttpException): 15 | def __init__(self, exception): 16 | super(NotReachableException, self).__init__(exception=exception) 17 | -------------------------------------------------------------------------------- /ddd/adapters/http/http_adapter.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | from ddd.adapters.adapter import Adapter 3 | 4 | 5 | class HttpAdapter(Adapter, metaclass=ABCMeta): 6 | def __init__(self, config, loop, log_service, webhook_callback_token): 7 | 8 | super().__init__( 9 | config=config, 10 | loop=loop, 11 | log_service=log_service 12 | ) 13 | 14 | self.webhook_callback_token = webhook_callback_token 15 | -------------------------------------------------------------------------------- /ddd/adapters/job/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/ddd/adapters/job/__init__.py -------------------------------------------------------------------------------- /ddd/adapters/job/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | The job adapter exceptions. 3 | """ 4 | class JobException(Exception): 5 | """ 6 | Base class. 7 | This class must not be used directly. 8 | """ 9 | def __init__(self, message): 10 | super().__init__(message) 11 | 12 | 13 | class JobNotFound(JobException): 14 | def __init__(self, job_id): 15 | super().__init__( 16 | f"Couldn't find job with ID: '{job_id}'." 17 | ) 18 | self.job_id = job_id 19 | -------------------------------------------------------------------------------- /ddd/adapters/job/job_adapter.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | 3 | 4 | class JobAdapter(object): 5 | """ 6 | The secondary 'scheduler' adapter schedules jobs. 7 | 8 | This is the primary 'jobs' adapter that is responsible for handling jobs 9 | when they are executed. 10 | """ 11 | 12 | def __init__(self, log_service, token): 13 | super().__init__() 14 | self.set_log_service(log_service) 15 | self.set_token(token) 16 | 17 | # Setters 18 | 19 | @abstractmethod 20 | def set_log_service(self, log_service): 21 | pass 22 | 23 | @abstractmethod 24 | def set_token(self, token): 25 | pass 26 | 27 | @abstractmethod 28 | def set_service(self, service): 29 | pass 30 | 31 | @classmethod 32 | async def run_job(cls, **kwargs): 33 | """ 34 | Does nothing since application logic (such as jobs) 35 | should be in your domain application service. 36 | 37 | Please override this class. 38 | """ 39 | raise Exception( 40 | "The base class run_job method was called. " 41 | "This should never happen. You must have scheduled a job " 42 | "without having implemented a job adapter to handle the job. " 43 | "Please override the class if you need jobs." 44 | ) 45 | -------------------------------------------------------------------------------- /ddd/adapters/job/scheduler_adapter.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | 3 | 4 | class SchedulerAdapter(object, metaclass=ABCMeta): 5 | def __init__(self, exec_func, log_service): 6 | super().__init__() 7 | self.exec_func = exec_func 8 | self.log_service = log_service 9 | 10 | @abstractmethod 11 | async def start(self): 12 | pass 13 | 14 | @abstractmethod 15 | async def stop(self): 16 | pass 17 | 18 | @abstractmethod 19 | async def add_cron_job( 20 | self, 21 | id, 22 | name, 23 | args, 24 | kwargs, 25 | crontab_expr, 26 | weekdays, 27 | hour, 28 | minute, 29 | second, 30 | start_date, 31 | end_date, 32 | timezone, 33 | coalesce=False, 34 | misfire_grace_time=(60 * 60), 35 | replace_existing=True, 36 | ): 37 | pass 38 | 39 | @abstractmethod 40 | async def add_date_job( 41 | self, 42 | id, 43 | name, 44 | args, 45 | kwargs, 46 | date, 47 | timezone, 48 | coalesce=False, 49 | misfire_grace_time=(60 * 60), 50 | replace_existing=True, 51 | ): 52 | pass 53 | 54 | @abstractmethod 55 | async def add_interval_job( 56 | self, 57 | id, 58 | name, 59 | args, 60 | kwargs, 61 | interval, 62 | days, 63 | hours, 64 | minutes, 65 | seconds, 66 | start_date, 67 | end_date, 68 | timezone, 69 | coalesce=False, 70 | misfire_grace_time=(60 * 60), 71 | replace_existing=True, 72 | ): 73 | pass 74 | 75 | @abstractmethod 76 | async def get_job(self, job_id): 77 | pass 78 | 79 | @abstractmethod 80 | async def remove_job(self, job_id, raises=False): 81 | pass 82 | -------------------------------------------------------------------------------- /ddd/application/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/ddd/application/__init__.py -------------------------------------------------------------------------------- /ddd/application/domain_registry.py: -------------------------------------------------------------------------------- 1 | class DomainRegistry(object): 2 | __instance = None 3 | 4 | @staticmethod 5 | def get_instance(): 6 | """ Static access method. """ 7 | if DomainRegistry.__instance is None: 8 | DomainRegistry() 9 | return DomainRegistry.__instance 10 | 11 | def __init__(self): 12 | """ Virtually private constructor. """ 13 | if DomainRegistry.__instance is not None: 14 | raise Exception("This class is a singleton!") 15 | else: 16 | DomainRegistry.__instance = self 17 | DomainRegistry.__instance.domain_publisher = None 18 | DomainRegistry.__instance.interchange_publisher = None 19 | DomainRegistry.__instance.task_db_conns = {} 20 | DomainRegistry.__instance.event_repository = None 21 | DomainRegistry.__instance.service = None 22 | DomainRegistry.__instance.log_service = None 23 | -------------------------------------------------------------------------------- /ddd/application/dummy_application_service.py: -------------------------------------------------------------------------------- 1 | from ddd.application.application_service import ApplicationService 2 | from ddd.application.action import action 3 | 4 | 5 | class DummyApplicationService(ApplicationService): 6 | """ 7 | This is the dummy context's application service. 8 | """ 9 | def __init__( 10 | self, 11 | db_service, 12 | domain_adapter, 13 | domain_publisher, 14 | event_repository, 15 | interchange_adapter, 16 | interchange_publisher, 17 | job_service, 18 | job_adapter, 19 | log_service, 20 | scheduler_adapter, 21 | dummy_repository, 22 | max_concurrent_actions, 23 | loop=None, 24 | ): 25 | """ 26 | Initialize the application service. 27 | """ 28 | super().__init__( 29 | db_service=db_service, 30 | domain_adapter=domain_adapter, 31 | domain_publisher=domain_publisher, 32 | event_repository=event_repository, 33 | interchange_adapter=interchange_adapter, 34 | interchange_publisher=interchange_publisher, 35 | job_service=job_service, 36 | job_adapter=job_adapter, 37 | log_service=log_service, 38 | scheduler_adapter=scheduler_adapter, 39 | max_concurrent_actions=max_concurrent_actions, 40 | loop=loop, 41 | ) 42 | 43 | # Dependencies 44 | self.dummy_repository = dummy_repository 45 | 46 | # ..categorize 47 | self.domain_services.extend([ 48 | 49 | ]) 50 | 51 | self.primary_adapters.extend([ 52 | 53 | ]) 54 | 55 | self.secondary_adapters.extend([ 56 | dummy_repository, 57 | ]) 58 | 59 | # Actions 60 | 61 | @action 62 | async def do_something(self, command, corr_ids=None): 63 | """ 64 | Do something. 65 | """ 66 | corr_ids = corr_ids if corr_ids is not None else [] 67 | 68 | raise NotImplementedError() 69 | -------------------------------------------------------------------------------- /ddd/domain/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/ddd/domain/__init__.py -------------------------------------------------------------------------------- /ddd/domain/aggregate.py: -------------------------------------------------------------------------------- 1 | from ddd.domain.entity import Entity 2 | 3 | 4 | class Aggregate(Entity): 5 | def __init__(self, version): 6 | super(Aggregate, self).__init__(version=version) 7 | -------------------------------------------------------------------------------- /ddd/domain/azure/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/ddd/domain/azure/__init__.py -------------------------------------------------------------------------------- /ddd/domain/azure/azure_event_publisher.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | 4 | from azure.eventhub.aio import EventHubProducerClient 5 | from azure.eventhub import EventData 6 | 7 | from ddd.domain.event_publisher import EventPublisher 8 | 9 | 10 | class AzureEventPublisher(EventPublisher): 11 | """ 12 | A azure event publisher. 13 | """ 14 | def __init__( 15 | self, 16 | namespace, 17 | namespace_conn_string, 18 | topic, 19 | group, 20 | log_service, 21 | keep_flushed_copies=False, 22 | ): 23 | super().__init__( 24 | log_service=log_service, 25 | keep_flushed_copies=keep_flushed_copies 26 | ) 27 | self.namespace = namespace 28 | self.namespace_conn_string = namespace_conn_string 29 | self.topic = topic 30 | self.group = group 31 | 32 | # Flush 33 | 34 | async def flush(self, event): 35 | """ 36 | Actually publishes the event, 37 | (called after request is done in the 'action' decorator). 38 | """ 39 | task_id = self._get_loop_task_id() 40 | 41 | if not self.producer: 42 | raise Exception( 43 | ( 44 | "Azure event publisher couldn't publish because " 45 | "producer has not been created." 46 | ) 47 | ) 48 | 49 | # Serialize 50 | try: 51 | json_data = json.dumps(event.serialize()).encode() 52 | except TypeError: 53 | self.log_service.error( 54 | "Failed to flush an event! The event couldn't be serialized, " 55 | "(check log details for event data).", 56 | extra={ 57 | 'event_data': event.serialize(), 58 | } 59 | ) 60 | raise 61 | 62 | # Without specifying partition_id or partition_key 63 | # the events will be distributed to available partitions via 64 | # round-robin. 65 | event_data_batch = await self.producer.create_batch() 66 | 67 | event_data_batch.add( 68 | EventData( 69 | json_data 70 | ) 71 | ) 72 | await self.producer.send_batch(event_data_batch) 73 | 74 | if self.keep_flushed_copies: 75 | if task_id not in self.flushed: 76 | self.flushed[task_id] = [] 77 | 78 | self.flushed[task_id].append(event) 79 | 80 | # Control 81 | 82 | async def start(self): 83 | await self.create_producer() 84 | 85 | async def stop(self): 86 | if not self.producer: 87 | self.log_service.warning( 88 | ( 89 | "Azure event publisher was instructed to stop " 90 | "but it doesn't seem to be started (no producer)." 91 | ) 92 | ) 93 | else: 94 | await self.producer.close() 95 | 96 | # Publishing 97 | 98 | async def create_producer(self): 99 | self.producer = \ 100 | EventHubProducerClient.from_connection_string( 101 | conn_str=self.namespace_conn_string, 102 | eventhub_name=self.topic, 103 | ) 104 | await asyncio.sleep(0.01) 105 | -------------------------------------------------------------------------------- /ddd/domain/building_block.py: -------------------------------------------------------------------------------- 1 | class BuildingBlock(object): 2 | def __init__(self, version): 3 | super(BuildingBlock, self).__init__() 4 | self.version = version 5 | 6 | def __eq__(self, other): 7 | if isinstance(other, self.__class__): 8 | a = self.__dict__ 9 | b = other.__dict__ 10 | return a == b 11 | else: 12 | return False 13 | 14 | def __ne__(self, other): 15 | return not self.__eq__(other) 16 | 17 | def equals(self, other, ignore_fields): 18 | ignore_fields = ignore_fields if ignore_fields is not None else [] 19 | 20 | if isinstance(other, self.__class__): 21 | 22 | from ddd.utils.utils import get_for_compare 23 | 24 | a = get_for_compare(self, ignore_fields) 25 | b = get_for_compare(other, ignore_fields) 26 | 27 | return a == b 28 | else: 29 | return False 30 | 31 | def serialize(self, ignore_fields=None): 32 | """ 33 | Serializes into a dict. 34 | """ 35 | from ddd.utils.utils import get_for_compare 36 | return get_for_compare(self, ignore_fields=ignore_fields) 37 | -------------------------------------------------------------------------------- /ddd/domain/command.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class Command(object): 4 | def __init__(self): 5 | super(Command, self).__init__() 6 | 7 | # Equality 8 | 9 | def __eq__(self, other): 10 | if isinstance(other, self.__class__): 11 | return True 12 | else: 13 | return False 14 | 15 | def __ne__(self, other): 16 | return not self.__eq__(other) 17 | -------------------------------------------------------------------------------- /ddd/domain/dummy/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/ddd/domain/dummy/__init__.py -------------------------------------------------------------------------------- /ddd/domain/dummy/dummy.py: -------------------------------------------------------------------------------- 1 | from ddd.domain.aggregate import Aggregate 2 | 3 | 4 | class Dummy(Aggregate): 5 | """ 6 | A 'dummy' for tests. 7 | """ 8 | VERSION = "2" 9 | 10 | def __init__( 11 | self, 12 | version, 13 | dummy_id, 14 | property_added, 15 | ): 16 | super().__init__(version=version) 17 | self.dummy_id = dummy_id 18 | self.property_added = property_added 19 | -------------------------------------------------------------------------------- /ddd/domain/dummy/dummy_id.py: -------------------------------------------------------------------------------- 1 | from ddd.domain.entity_id import EntityId 2 | 3 | 4 | class DummyId(EntityId): 5 | """ 6 | A patient ID. 7 | """ 8 | def __init__(self, identity): 9 | super(DummyId, self).__init__(identity=identity) 10 | -------------------------------------------------------------------------------- /ddd/domain/dummy/dummy_repository.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | 3 | 4 | class DummyRepository(object, metaclass=ABCMeta): 5 | def __init__( 6 | self, 7 | ): 8 | DummyRepository.__init__(self) 9 | 10 | # Operations 11 | 12 | 13 | 14 | # Migration 15 | 16 | def _migrate_v1_to_v2(self, record): 17 | """ 18 | Add: 'property_added' 19 | """ 20 | record['data']['property_added'] = "in_version_2" 21 | 22 | return record 23 | -------------------------------------------------------------------------------- /ddd/domain/dummy/dummy_translator.py: -------------------------------------------------------------------------------- 1 | from ddd.adapters.message_reader import MessageReader 2 | 3 | from ddd.utils.utils import str_or_none 4 | 5 | 6 | class DummyTranslator(object): 7 | def __init__(self, dummy): 8 | super().__init__() 9 | self.dummy = dummy 10 | 11 | def to_interchange(self): 12 | return self.to_dict() 13 | 14 | def to_domain(self): 15 | return self.to_dict() 16 | 17 | @classmethod 18 | def from_domain(cls, the_dict): 19 | from ddd.domain.dummy.dummy import Dummy 20 | from ddd.domain.dummy.dummy_id import DummyId 21 | 22 | reader = MessageReader(the_dict) 23 | 24 | # Read 25 | dummy = Dummy( 26 | version=reader.string_value('version'), 27 | dummy_id=reader.entity_id_value('dummy_id', DummyId), 28 | property_added=reader.string_value('property_added'), 29 | ) 30 | 31 | return dummy 32 | 33 | def to_record(self): 34 | return self.to_dict() 35 | 36 | @classmethod 37 | def from_record(cls, record): 38 | the_dict = record['data'] 39 | the_dict['dummy_id'] = record['id'] 40 | 41 | return DummyTranslator.from_domain(the_dict) 42 | 43 | def to_dict(self): 44 | return { 45 | 'version': self.dummy.version, 46 | 'dummy_id': str_or_none(self.dummy.dummy_id), 47 | 'property_added': self.dummy.property_added, 48 | } 49 | -------------------------------------------------------------------------------- /ddd/domain/entity.py: -------------------------------------------------------------------------------- 1 | from ddd.domain.building_block import BuildingBlock 2 | 3 | 4 | class Entity(BuildingBlock): 5 | def __init__(self, version): 6 | super(Entity, self).__init__(version=version) 7 | -------------------------------------------------------------------------------- /ddd/domain/entity_id.py: -------------------------------------------------------------------------------- 1 | from ddd.domain.value_object import ValueObject 2 | 3 | 4 | class EntityId(ValueObject): 5 | def __init__(self, identity, version="1"): 6 | super(EntityId, self).__init__(version=version) 7 | self.identity = str(identity) 8 | 9 | def __str__(self): 10 | return self.identity 11 | 12 | def __eq__(self, other): 13 | if not isinstance(other, EntityId): 14 | return self.identity == str(other) 15 | 16 | return self.identity == other.identity 17 | -------------------------------------------------------------------------------- /ddd/domain/entity_id_translator.py: -------------------------------------------------------------------------------- 1 | class EntityIdTranslator(object): 2 | def __init__(self, entity_id): 3 | super().__init__() 4 | self.entity_id = entity_id 5 | 6 | @classmethod 7 | def from_domain(cls, identity, entity_class): 8 | entity_id = entity_class(identity=identity) 9 | return entity_id 10 | -------------------------------------------------------------------------------- /ddd/domain/event/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/ddd/domain/event/__init__.py -------------------------------------------------------------------------------- /ddd/domain/event/domain_event.py: -------------------------------------------------------------------------------- 1 | from ddd.domain.event.event import Event 2 | 3 | 4 | class DomainEvent(Event): 5 | """ 6 | An domain event base class. 7 | 8 | .. note:: 9 | This class should never be instantiated by the user. 10 | 11 | Subclass and implement 12 | :meth:`~ddd.domain.event.event.Event.get_serialized_payload`. 13 | """ 14 | def __init__(self, name, version="1", date=None, sender=None, corr_ids=None): 15 | super(DomainEvent, self).__init__( 16 | name=name, 17 | version=version, 18 | date=date, 19 | sender=sender, 20 | corr_ids=corr_ids, 21 | ) 22 | -------------------------------------------------------------------------------- /ddd/domain/event/event.py: -------------------------------------------------------------------------------- 1 | import arrow 2 | import copy 3 | 4 | from abc import ABCMeta, abstractmethod 5 | 6 | from ddd.utils.utils import get_for_compare 7 | 8 | 9 | class Event(object, metaclass=ABCMeta): 10 | """ 11 | The event base class. 12 | """ 13 | def __init__(self, name, version="1", date=None, sender=None, corr_ids=None): 14 | date = date if date is not None else arrow.utcnow() 15 | corr_ids = corr_ids if corr_ids is not None else [] 16 | 17 | self.name = name 18 | self.version = version 19 | self.date = date 20 | self.sender = sender 21 | self.corr_ids = corr_ids 22 | 23 | def equals(self, other, ignore_fields): 24 | ignore_fields = ignore_fields if ignore_fields is not None else [] 25 | 26 | if isinstance(other, self.__class__): 27 | 28 | a = get_for_compare(self, ignore_fields) 29 | b = get_for_compare(other, ignore_fields) 30 | 31 | return a == b 32 | else: 33 | return False 34 | 35 | def __eq__(self, other): 36 | if isinstance(other, self.__class__): 37 | a = copy.deepcopy(self.__dict__) 38 | b = copy.deepcopy(other.__dict__) 39 | a.pop('date') 40 | b.pop('date') 41 | return a == b 42 | else: 43 | return False 44 | 45 | def __ne__(self, other): 46 | return not self.__eq__(other) 47 | 48 | @abstractmethod 49 | def get_serialized_payload(self): 50 | """ 51 | Get the serialized payload (the event data). 52 | 53 | :rtype: dict 54 | """ 55 | pass 56 | 57 | def serialize(self): 58 | obj = { 59 | 'name': self.name, 60 | 'version': self.version, 61 | 'date': self.date.isoformat(), 62 | 'sender': self.sender, 63 | 'payload': self.get_serialized_payload(), 64 | 'corr_ids': self.corr_ids, 65 | } 66 | return obj 67 | -------------------------------------------------------------------------------- /ddd/domain/event/integration_event.py: -------------------------------------------------------------------------------- 1 | from ddd.domain.event.event import Event 2 | 3 | 4 | class IntegrationEvent(Event): 5 | """ 6 | An integration event. 7 | 8 | .. note:: 9 | This class should never be instantiated by the user. 10 | 11 | Subclass and implement 12 | :meth:`~ddd.domain.event.event.Event.get_serialized_payload`. 13 | """ 14 | def __init__(self, name, version="1", date=None, sender=None, corr_ids=None): 15 | super(IntegrationEvent, self).__init__( 16 | name=name, 17 | version=version, 18 | date=date, 19 | sender=sender, 20 | corr_ids=corr_ids, 21 | ) 22 | -------------------------------------------------------------------------------- /ddd/domain/event_publisher.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from abc import ABCMeta, abstractmethod 4 | 5 | from ddd.application.domain_registry import DomainRegistry 6 | 7 | 8 | class EventPublisher(object, metaclass=ABCMeta): 9 | """ 10 | The event publisher interface. 11 | """ 12 | def __init__(self, log_service=None, keep_flushed_copies=False): 13 | 14 | self.log_service = \ 15 | log_service \ 16 | if log_service \ 17 | else DomainRegistry.get_instance().log_service 18 | 19 | self.keep_flushed_copies = keep_flushed_copies 20 | 21 | self.published = {} 22 | self.flushed = {} 23 | 24 | # To override 25 | 26 | @abstractmethod 27 | async def start(self): 28 | pass 29 | 30 | @abstractmethod 31 | async def stop(self): 32 | pass 33 | 34 | @abstractmethod 35 | async def flush(self, event): 36 | """ 37 | Flush (actually publish) the events. 38 | """ 39 | pass 40 | 41 | # Methods 42 | 43 | async def publish(self, event): 44 | """ 45 | Publish an event. 46 | """ 47 | task_id = self._get_loop_task_id() 48 | 49 | if task_id not in self.published: 50 | self.published[task_id] = [] 51 | 52 | # Log 53 | extra = {} 54 | 55 | self.log_service.debug( 56 | f"Publishing event: {event.name}", 57 | extra=extra, 58 | ) 59 | 60 | self.published[task_id].append(event) 61 | 62 | def get_published(self): 63 | task_id = self._get_loop_task_id() 64 | 65 | if task_id in self.published: 66 | return self.published[task_id] 67 | 68 | return [] 69 | 70 | def clear_published(self): 71 | task_id = self._get_loop_task_id() 72 | 73 | if task_id in self.published: 74 | self.published[task_id] = [] 75 | 76 | def has_flushed(self, event, ignore_fields=None): 77 | ignore_fields = ignore_fields if ignore_fields is not None else [] 78 | 79 | for task_id in self.flushed: 80 | for e in self.flushed[task_id]: 81 | if e.equals(event, ignore_fields=ignore_fields): 82 | return True 83 | return False 84 | 85 | # Helpers 86 | 87 | def _get_loop_task_id(self): 88 | """ 89 | Get currently executing asyncio event loop task. 90 | """ 91 | if hasattr(asyncio, "current_task"): # python >= 3.7 92 | task_id = id(asyncio.current_task()) 93 | elif hasattr(asyncio.Task, "current_task"): # python >= 3.6 < 3.7 94 | task_id = id(asyncio.Task.current_task()) 95 | else: 96 | raise Exception( 97 | "Couldn't get currently running task on asyncio event loop. " 98 | "The version of python you're using isn't supported by " 99 | "this library." 100 | ) 101 | -------------------------------------------------------------------------------- /ddd/domain/exceptions.py: -------------------------------------------------------------------------------- 1 | class DomainException(Exception): 2 | """ 3 | A domain exception. 4 | """ 5 | def __init__(self, errors): 6 | super(DomainException, self).__init__() 7 | self.errors = errors 8 | 9 | def __str__(self): 10 | return ",".join([e['message'] for e in self.errors]) 11 | -------------------------------------------------------------------------------- /ddd/domain/kafka/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/ddd/domain/kafka/__init__.py -------------------------------------------------------------------------------- /ddd/domain/memory/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/ddd/domain/memory/__init__.py -------------------------------------------------------------------------------- /ddd/domain/memory/memory_event_publisher.py: -------------------------------------------------------------------------------- 1 | from ddd.domain.event_publisher import EventPublisher 2 | 3 | 4 | class MemoryEventPublisher(EventPublisher): 5 | """ 6 | A memory event publisher. 7 | """ 8 | def __init__( 9 | self, 10 | log_service=None, 11 | keep_flushed_copies=False, 12 | ): 13 | super().__init__( 14 | log_service=log_service, 15 | keep_flushed_copies=keep_flushed_copies 16 | ) 17 | self.is_started = False 18 | self.primary_adapter = None 19 | 20 | # Flush 21 | 22 | async def flush(self, event): 23 | """ 24 | Actually publishes the event, 25 | (called after request is done in the 'action' decorator). 26 | """ 27 | if not self.is_started: 28 | raise Exception( 29 | "Can't flush the domain event because " 30 | "domain publisher isn't started." 31 | ) 32 | 33 | task_id = self._get_loop_task_id() 34 | 35 | if self.primary_adapter: 36 | for listener in self.primary_adapter.listeners: 37 | if listener.listens_to(event.name): 38 | try: 39 | await listener.handle(event.serialize()) 40 | except Exception as e: 41 | self.log_service.error( 42 | "Listener failed to handle event '{}', " 43 | "exception: {}". 44 | format( 45 | event.name, 46 | str(e), 47 | ), 48 | exc_info=True, 49 | ) 50 | else: 51 | self.log_service.warning( 52 | "Couldn't flush events because no primary adapter is set." 53 | ) 54 | 55 | if self.keep_flushed_copies: 56 | if task_id not in self.flushed: 57 | self.flushed[task_id] = [] 58 | 59 | self.flushed[task_id].append(event) 60 | 61 | # Control 62 | 63 | async def start(self): 64 | self.is_started = True 65 | 66 | async def stop(self): 67 | self.is_started = False 68 | 69 | # Setters 70 | 71 | def set_primary_adapter(self, primary_adapter): 72 | self.primary_adapter = primary_adapter 73 | -------------------------------------------------------------------------------- /ddd/domain/service.py: -------------------------------------------------------------------------------- 1 | class DomainService(object): 2 | """ 3 | A domain service base class. 4 | """ 5 | def __init__(self): 6 | super(DomainService, self).__init__() 7 | -------------------------------------------------------------------------------- /ddd/domain/value_object.py: -------------------------------------------------------------------------------- 1 | from ddd.domain.building_block import BuildingBlock 2 | 3 | 4 | class ValueObject(BuildingBlock): 5 | def __init__(self, version): 6 | super(ValueObject, self).__init__(version=version) 7 | -------------------------------------------------------------------------------- /ddd/infrastructure/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/ddd/infrastructure/__init__.py -------------------------------------------------------------------------------- /ddd/infrastructure/container.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import signal 3 | 4 | 5 | class Container(object): 6 | """ 7 | A container for hosting an application service. 8 | """ 9 | def __init__(self, app_service, log_service, loop=None): 10 | super().__init__() 11 | 12 | # Deps 13 | self.app_service = app_service 14 | self.log_service = log_service 15 | self.loop = loop if loop else asyncio.get_event_loop() 16 | 17 | # Vars 18 | self.stop_sem = None 19 | 20 | # Docker 21 | self._create_stop_semaphore() 22 | self._subscribe_to_signals() 23 | 24 | # Docker 25 | 26 | def _create_stop_semaphore(self): 27 | """ 28 | Create the stop semaphore that we will block on after application 29 | service has been started. 30 | 31 | Acquiring the semaphore means the container has been ordered to stop. 32 | """ 33 | self.stop_sem = asyncio.Event() 34 | 35 | def _subscribe_to_signals(self): 36 | """ 37 | Subscribe to SIGINT (ctrl + c) and SIGTERM (docker stop). 38 | """ 39 | for signal_name in ('SIGINT', 'SIGTERM'): 40 | self.loop.add_signal_handler( 41 | getattr(signal, signal_name), 42 | lambda: self.loop.create_task(self._handle_signal(signal_name)) 43 | ) 44 | 45 | async def _handle_signal(self, signal_name): 46 | """ 47 | Handler for process signals. 48 | """ 49 | self.log_service.debug(f"Received system signal: '{signal_name}'.") 50 | 51 | if signal_name in ['SIGTERM', 'SIGKILL']: 52 | self.stop_sem.set() 53 | 54 | # Run 55 | 56 | async def run(self): 57 | """ 58 | Run the app, (called on container start). 59 | """ 60 | await self.app_service.start() 61 | await self.wait_for_stop() 62 | await self.stop() 63 | 64 | async def wait_for_stop(self): 65 | """ 66 | Block waiting for termination/stop signals. 67 | """ 68 | self.log_service.info("Waiting for stop/term signals..") 69 | await self.stop_sem.wait() 70 | 71 | async def stop(self): 72 | self.log_service.info("Stopping app..") 73 | await self.app_service.stop() 74 | -------------------------------------------------------------------------------- /ddd/infrastructure/db_service/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/ddd/infrastructure/db_service/__init__.py -------------------------------------------------------------------------------- /ddd/infrastructure/db_service/db_service.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | 3 | from ddd.infrastructure.infrastructure_service import InfrastructureService 4 | 5 | 6 | class DbService(InfrastructureService, metaclass=ABCMeta): 7 | """ 8 | The db service base class. 9 | 10 | :param log_service: the log service. 11 | """ 12 | def __init__( 13 | self, 14 | log_service, 15 | ): 16 | super().__init__(log_service=log_service) 17 | self.conn_pool = None 18 | 19 | @abstractmethod 20 | async def _create_conn_pool(self): 21 | pass 22 | 23 | async def start(self): 24 | """ 25 | Starts the db service. 26 | """ 27 | self.log_service.info("..starting db service") 28 | await self._create_conn_pool() 29 | 30 | async def stop(self): 31 | """ 32 | Stops the db service. 33 | """ 34 | self.log_service.info("..stopping db service") 35 | await self.conn_pool.close() 36 | -------------------------------------------------------------------------------- /ddd/infrastructure/db_service/memory_db_service.py: -------------------------------------------------------------------------------- 1 | from ddd.infrastructure.db_service.db_service import DbService 2 | from ddd.infrastructure.db_service.memory_pool import MemoryPool 3 | 4 | 5 | class MemoryDbService(DbService): 6 | """ 7 | A memory db service. 8 | 9 | :param log_service: the log service. 10 | """ 11 | def __init__( 12 | self, 13 | log_service, 14 | ): 15 | super().__init__(log_service=log_service) 16 | 17 | async def _create_conn_pool(self): 18 | self.log_service.info("Creating memory db pool") 19 | self.conn_pool = MemoryPool(max=80) 20 | -------------------------------------------------------------------------------- /ddd/infrastructure/db_service/memory_pool.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | 4 | class Transaction(object): 5 | def __init__(self): 6 | super(Transaction, self).__init__() 7 | 8 | async def __aenter__(self): 9 | return "dummy-transaction" 10 | 11 | async def __aexit__(self, type, value, traceback): 12 | pass 13 | 14 | 15 | class Conn(object): 16 | def __init__(self): 17 | super(Conn, self).__init__() 18 | self.is_open = False 19 | 20 | def open(self): 21 | self.is_open = True 22 | 23 | def close(self): 24 | self.is_open = False 25 | 26 | def transaction(self): 27 | return Transaction() 28 | 29 | async def __aenter__(self): 30 | self.is_open = True 31 | 32 | async def __aexit__(self, type, value, traceback): 33 | self.is_open = False 34 | 35 | 36 | class ConnectionHolder(object): 37 | def __init__(self): 38 | super(ConnectionHolder, self).__init__() 39 | 40 | async def __aenter__(self): 41 | self.conn = Conn() 42 | self.conn.open() 43 | return self.conn 44 | 45 | async def __aexit__(self, type, value, traceback): 46 | self.conn.close() 47 | 48 | 49 | class MemoryPool(object): 50 | """ 51 | The memory pool is used in the action decorator 52 | to create a "memory" transaction (for tests). 53 | """ 54 | def __init__(self, max=80): 55 | super(MemoryPool, self).__init__() 56 | self.sem = threading.Semaphore(value=max) 57 | 58 | def acquire(self): 59 | self.sem.acquire() 60 | return ConnectionHolder() 61 | 62 | def release(self): 63 | self.sem.release() 64 | 65 | async def close(self): 66 | pass 67 | -------------------------------------------------------------------------------- /ddd/infrastructure/db_service/memory_postgres_db_service.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import asyncpg 3 | import json 4 | 5 | from testing import postgresql 6 | 7 | from ddd.infrastructure.db_service.db_service import DbService 8 | 9 | 10 | class MemoryPostgresDbService(DbService): 11 | """ 12 | A in-memory postgres db service. 13 | 14 | :param log_service: the log service. 15 | :param min_size: minimum number of connections in the db pool. 16 | :type min_size: int, optional 17 | :param max_size: maximum number of connections in the db pool. 18 | :type max_size: int, optional 19 | """ 20 | def __init__( 21 | self, 22 | log_service, 23 | min_size=20, 24 | max_size=20, 25 | ): 26 | super().__init__(log_service=log_service) 27 | self.dsn = None 28 | self.postgres = None 29 | self.min_size = min_size 30 | self.max_size = max_size 31 | 32 | async def start(self): 33 | """ 34 | Starts the db service. 35 | """ 36 | self.postgres = postgresql.Postgresql() 37 | self.dsn = self.postgres.url() 38 | await super().start() 39 | 40 | async def stop(self): 41 | """ 42 | Stops the db service. 43 | """ 44 | await super().stop() 45 | self.postgres.stop() 46 | 47 | async def _create_conn_pool(self): 48 | self.log_service.info( 49 | f"..creating connection pool " 50 | f"(min: {self.min_size}, max: {self.max_size}).." 51 | ) 52 | 53 | created = False 54 | error = None 55 | backoff = 6 56 | retries = 0 57 | max_retries = 5 58 | 59 | async def init_conn(conn): 60 | await conn.set_type_codec( 61 | 'json', 62 | encoder=json.dumps, 63 | decoder=json.loads, 64 | schema='pg_catalog' 65 | ) 66 | 67 | while (not created) and retries < max_retries: 68 | try: 69 | pool = \ 70 | await asyncpg.create_pool( 71 | dsn=self.dsn, 72 | min_size=self.min_size, 73 | max_size=self.max_size, 74 | init=init_conn 75 | ) 76 | created = True 77 | except Exception as e: 78 | self.log_service.error( 79 | ( 80 | "Failed to create connection pool of postgres db " 81 | "at: {} (retrying in {} secs..), exception: {}" 82 | ). 83 | format( 84 | self.dsn, 85 | backoff, 86 | str(e) 87 | ) 88 | ) 89 | error = str(e) 90 | retries += 1 91 | await asyncio.sleep(backoff) 92 | 93 | if not created: 94 | raise Exception( 95 | ( 96 | "App failed to create connection pool of db at: {} " 97 | "after {} retries, error: {}." 98 | ). 99 | format( 100 | self.dsn, 101 | retries, 102 | error 103 | ) 104 | ) 105 | 106 | self.conn_pool = pool 107 | -------------------------------------------------------------------------------- /ddd/infrastructure/db_service/postgres_db_service.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import asyncpg 3 | import json 4 | 5 | from ddd.infrastructure.db_service.db_service import DbService 6 | 7 | 8 | class PostgresDbService(DbService): 9 | """ 10 | A postgres db service. 11 | 12 | :param dsn: the dsn (connection string). 13 | :param log_service: the log service. 14 | :param min_size: minimum number of connections in the db pool. 15 | :type min_size: int, optional 16 | :param max_size: maximum number of connections in the db pool. 17 | :type max_size: int, optional 18 | """ 19 | def __init__( 20 | self, 21 | dsn, 22 | log_service, 23 | min_size=20, 24 | max_size=20, 25 | ): 26 | super().__init__(log_service=log_service) 27 | self.dsn = dsn 28 | self.min_size = min_size 29 | self.max_size = max_size 30 | 31 | async def _create_conn_pool(self): 32 | self.log_service.info( 33 | f"..creating connection pool " 34 | f"(min: {self.min_size}, max: {self.max_size}).." 35 | ) 36 | 37 | created = False 38 | error = None 39 | backoff = 6 40 | retries = 0 41 | max_retries = 5 42 | 43 | async def init_conn(conn): 44 | await conn.set_type_codec( 45 | 'json', 46 | encoder=json.dumps, 47 | decoder=json.loads, 48 | schema='pg_catalog' 49 | ) 50 | 51 | while (not created) and retries < max_retries: 52 | try: 53 | pool = \ 54 | await asyncpg.create_pool( 55 | dsn=self.dsn, 56 | min_size=self.min_size, 57 | max_size=self.max_size, 58 | init=init_conn 59 | ) 60 | created = True 61 | except Exception as e: 62 | self.log_service.error( 63 | ( 64 | "Failed to create connection pool of postgres db " 65 | "at: {} (retrying in {} secs..), exception: {}" 66 | ). 67 | format( 68 | self.dsn, 69 | backoff, 70 | str(e) 71 | ) 72 | ) 73 | error = str(e) 74 | retries += 1 75 | await asyncio.sleep(backoff) 76 | 77 | if not created: 78 | raise Exception( 79 | ( 80 | "App failed to create connection pool of db at: {} " 81 | "after {} retries, error: {}." 82 | ). 83 | format( 84 | self.dsn, 85 | retries, 86 | error 87 | ) 88 | ) 89 | 90 | self.conn_pool = pool 91 | -------------------------------------------------------------------------------- /ddd/infrastructure/email_service.py: -------------------------------------------------------------------------------- 1 | from ddd.infrastructure.infrastructure_service import InfrastructureService 2 | 3 | 4 | class EmailService(InfrastructureService): 5 | """ 6 | An email infrastructure service used to send emails. 7 | """ 8 | def __init__( 9 | self, 10 | logger=None, 11 | ): 12 | super().__init__(logger=logger) 13 | 14 | async def send( 15 | self, 16 | from_, 17 | to_, 18 | subject, 19 | content, 20 | ): 21 | raise NotImplementedError() 22 | -------------------------------------------------------------------------------- /ddd/infrastructure/infrastructure_service.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | 3 | 4 | class InfrastructureService(object, metaclass=ABCMeta): 5 | """ 6 | A infrastructure service base class. 7 | """ 8 | def __init__(self, log_service): 9 | super().__init__() 10 | 11 | self.log_service = log_service 12 | 13 | @abstractmethod 14 | async def start(self): 15 | pass 16 | 17 | @abstractmethod 18 | async def stop(self): 19 | pass 20 | -------------------------------------------------------------------------------- /ddd/repositories/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/ddd/repositories/__init__.py -------------------------------------------------------------------------------- /ddd/repositories/memory/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/ddd/repositories/memory/__init__.py -------------------------------------------------------------------------------- /ddd/repositories/memory/memory_dummy_repository.py: -------------------------------------------------------------------------------- 1 | from ddd.repositories.memory.memory_repository import MemoryRepository 2 | 3 | from ddd.domain.dummy.dummy import Dummy 4 | from ddd.domain.dummy.dummy_repository import DummyRepository 5 | from ddd.domain.dummy.dummy_translator import DummyTranslator 6 | 7 | 8 | class MemoryDummyRepository(DummyRepository, MemoryRepository): 9 | 10 | def __init__(self, log_service): 11 | MemoryRepository.__init__( 12 | self, 13 | log_service=log_service, 14 | aggregate_name="dummy", 15 | aggregate_cls=Dummy, 16 | translator_cls=DummyTranslator, 17 | ) 18 | 19 | async def get(self, dummy_id): 20 | return await super()._get(dummy_id) 21 | 22 | async def save(self, dummy): 23 | await super()._save( 24 | aggregate_id=dummy.dummy_id, 25 | aggregate=dummy, 26 | ) 27 | 28 | async def delete(self, dummy): 29 | await self._delete(dummy.dummy_id) 30 | -------------------------------------------------------------------------------- /ddd/repositories/memory/memory_event_repository.py: -------------------------------------------------------------------------------- 1 | class MemoryEventRepository(object): 2 | def __init__(self): 3 | super(MemoryEventRepository, self).__init__() 4 | self.events = { 5 | 'domain': {}, 6 | 'integration': {}, 7 | } 8 | 9 | async def delete_all(self, action_id, event_type): 10 | self.events[event_type].pop(action_id, None) 11 | 12 | async def get_unpublished(self, action_id, event_type): 13 | if action_id in self.events[event_type]: 14 | return self.events[event_type][action_id] 15 | return [] 16 | 17 | async def save_all(self, action_id, events, event_type): 18 | self.events[event_type][action_id] = events 19 | -------------------------------------------------------------------------------- /ddd/repositories/memory/memory_repository.py: -------------------------------------------------------------------------------- 1 | from ddd.repositories.repository import Repository 2 | 3 | 4 | class MemoryRepository(Repository): 5 | 6 | def __init__( 7 | self, 8 | log_service, 9 | aggregate_name, 10 | aggregate_cls, 11 | translator_cls, 12 | ): 13 | super().__init__( 14 | log_service=log_service, 15 | aggregate_cls=aggregate_cls, 16 | translator_cls=translator_cls, 17 | ) 18 | 19 | self.highest_id = 0 20 | self.highest_id_collector = 0 21 | self.records = {} 22 | 23 | self.aggregate_name = aggregate_name 24 | 25 | async def next_identity(self): 26 | self.highest_id += 1 27 | return str(self.highest_id) 28 | 29 | async def next_collector_identity(self): 30 | self.highest_id_collector += 1 31 | return str(self.highest_id_collector) 32 | 33 | # Operations 34 | 35 | async def _get_record(self, aggregate_id): 36 | if str(aggregate_id) in self.records: 37 | return self.records[str(aggregate_id)] 38 | return None 39 | 40 | async def _get_all_records(self): 41 | return self.records.values() 42 | 43 | async def _get_all_records_not_on_latest_version(self): 44 | records = [] 45 | 46 | for r in self.records.values(): 47 | if int(r['data']['version']) < int(self.aggregate_cls.VERSION): 48 | records.append(r) 49 | 50 | return records 51 | 52 | async def _save_record(self, aggregate_id, data): 53 | aggregate_id = str(aggregate_id) 54 | 55 | if aggregate_id in self.records: 56 | self.records.pop(aggregate_id) 57 | 58 | self.records[aggregate_id] = { 59 | 'id': aggregate_id, 60 | 'data': data, 61 | } 62 | 63 | async def _delete(self, aggregate_id): 64 | self.records = { 65 | agg_id: data for agg_id, data in self.records.items() 66 | if agg_id != aggregate_id 67 | } 68 | 69 | async def get_count(self): 70 | return len(self.records) 71 | 72 | async def get_single_or_raise(self): 73 | """ 74 | Convenience method for tests to get a single aggregate. 75 | Throws exception if not exactly 1 aggregate in repository. 76 | """ 77 | aggregates = await self.get_all() 78 | 79 | if len(aggregates) != 1: 80 | raise Exception( 81 | "Couldn't get single aggregate because there are not " 82 | "exactly 1 aggregate in the repository." 83 | ) 84 | 85 | return aggregates[0] 86 | 87 | async def get_all(self): 88 | records = await self._get_all_records() 89 | 90 | records = self._migrate(records, self.aggregate_cls) 91 | 92 | aggregates = [ 93 | self.aggregate_from_record(record) 94 | for record in records 95 | ] 96 | 97 | return aggregates 98 | 99 | async def delete_all(self): 100 | self.highest_id = 0 101 | self.records = {} 102 | 103 | # Control 104 | 105 | async def start(self): 106 | pass 107 | 108 | async def stop(self): 109 | pass 110 | 111 | async def connect(self): 112 | pass 113 | 114 | async def assert_table(self): 115 | pass 116 | 117 | async def disconnect(self): 118 | pass 119 | 120 | # Clean 121 | 122 | async def empty(self): 123 | self.highest_id = 0 124 | self.records = {} 125 | -------------------------------------------------------------------------------- /ddd/repositories/postgres/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/ddd/repositories/postgres/__init__.py -------------------------------------------------------------------------------- /ddd/repositories/postgres/postgres_dummy_repository.py: -------------------------------------------------------------------------------- 1 | from ddd.repositories.postgres.postgres_repository import PostgresRepository 2 | 3 | from ddd.domain.dummy.dummy import Dummy 4 | from ddd.domain.dummy.dummy_repository import DummyRepository 5 | from ddd.domain.dummy.dummy_translator import DummyTranslator 6 | 7 | 8 | class PostgresDummyRepository(DummyRepository, PostgresRepository): 9 | def __init__( 10 | self, 11 | config, 12 | db_service, 13 | log_service, 14 | loop=None, 15 | ): 16 | PostgresRepository.__init__( 17 | self, 18 | loop=loop, 19 | db_service=db_service, 20 | log_service=log_service, 21 | aggregate_cls=Dummy, 22 | translator_cls=DummyTranslator, 23 | table_name="ddd_dummies", 24 | dsn=config.database.postgres.dsn, 25 | ) 26 | 27 | # Operations 28 | 29 | async def get(self, dummy_id): 30 | return await self._get(aggregate_id=dummy_id) 31 | 32 | async def save(self, dummy): 33 | await self._save( 34 | aggregate_id=dummy.dummy_id, 35 | aggregate=dummy, 36 | ) 37 | 38 | async def delete(self, dummy): 39 | await self._delete(aggregate_id=dummy.dummy_id) 40 | -------------------------------------------------------------------------------- /ddd/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/ddd/tests/__init__.py -------------------------------------------------------------------------------- /ddd/tests/adapters/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/ddd/tests/adapters/__init__.py -------------------------------------------------------------------------------- /ddd/tests/adapters/event/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/ddd/tests/adapters/event/__init__.py -------------------------------------------------------------------------------- /ddd/tests/adapters/event/test_event_adapter.py: -------------------------------------------------------------------------------- 1 | from unittest import skip 2 | 3 | from ddd.tests.base_test_case import BaseTestCase 4 | 5 | from ddd.adapters.event.memory.memory_event_adapter import MemoryEventAdapter 6 | 7 | 8 | class TestEventAdapter(BaseTestCase): 9 | 10 | async def asyncSetUp(self): 11 | await super().asyncSetUp() 12 | 13 | self.adapter = MemoryEventAdapter() 14 | 15 | @skip 16 | async def test_assigns_service_to_listeners_when_setting_service( 17 | self, 18 | ): 19 | # Setup 20 | listener = \ 21 | CustomerAccountCreatedListener( 22 | action="send_welcome_email", 23 | service=None, 24 | command_creator=lambda event: 25 | SendWelcomeEmailCommand( 26 | customer_id=event.customer_id, 27 | token=None, 28 | ) 29 | ) 30 | 31 | self.adapter.set_listeners([ 32 | listener 33 | ]) 34 | 35 | service = WebshopApplicationService( 36 | db_service=None, 37 | domain_adapter=None, 38 | domain_publisher=None, 39 | event_repository=None, 40 | interchange_adapter=None, 41 | interchange_publisher=None, 42 | job_service=None, 43 | job_adapter=None, 44 | log_service=None, 45 | scheduler_adapter=None, 46 | customer_repository=None, 47 | max_concurrent_actions=10, 48 | ) 49 | 50 | # Exercise 51 | self.adapter.set_service(service) 52 | 53 | # Assert 54 | for listener in self.adapter.listeners: 55 | self.assertEqual(service, listener.service) 56 | 57 | @skip 58 | async def test_assigns_service_to_listeners_when_setting_listeners( 59 | self, 60 | ): 61 | raise NotImplementedError() 62 | 63 | @skip 64 | async def test_delegates_event_to_listeners( 65 | self, 66 | ): 67 | raise NotImplementedError() 68 | -------------------------------------------------------------------------------- /ddd/tests/base_test_case.py: -------------------------------------------------------------------------------- 1 | from unittest.async_case import IsolatedAsyncioTestCase 2 | 3 | from abc import abstractmethod 4 | 5 | from ddd.utils.utils import load_env_file 6 | 7 | 8 | class BaseTestCase(IsolatedAsyncioTestCase): 9 | 10 | def __init__(self, methodName='runTest'): 11 | super(BaseTestCase, self).__init__(methodName=methodName) 12 | 13 | async def asyncSetUp(self): 14 | await super(BaseTestCase, self).asyncSetUp() 15 | 16 | # Vars 17 | self.config = None 18 | self.deps = None 19 | self.loop = None 20 | 21 | # Load env vars 22 | load_env_file() 23 | 24 | # Read config 25 | self.read_config() 26 | 27 | async def asyncTearDown(self): 28 | await super().asyncTearDown() 29 | 30 | @abstractmethod 31 | def read_config(self): 32 | """ 33 | Read the config into 'self.config'. 34 | """ 35 | pass 36 | 37 | # Assert 38 | 39 | def assertEqualIgnoringId(self, id_field, a, b): 40 | """ 41 | Convenience method for 'assertEqualIgnoringIds'. 42 | """ 43 | self.assertEqualIgnoringIds( 44 | id_fields=[id_field], 45 | a=a, 46 | b=b, 47 | ) 48 | 49 | def assertEqualIgnoringIds(self, id_fields, a, b): 50 | """ 51 | This is an assertion method used to assert two entities 52 | are equal, disregarding some entitiy IDs. 53 | """ 54 | self.assertEqualIgnoringFields( 55 | ignore_fields=id_fields, 56 | a=a, 57 | b=b, 58 | ) 59 | 60 | def assertEqualIgnoringFields(self, a, b, ignore_fields=None): 61 | """ 62 | This is an assertion method used to assert two entities 63 | are equal, disregarding some entitiy IDs. 64 | """ 65 | ignore_fields = ignore_fields if ignore_fields is not None else [] 66 | 67 | if type(a) == list: 68 | self.assertListsEqualIgnoringFields(a, b, ignore_fields) 69 | else: 70 | if not a.equals(b, ignore_fields=ignore_fields): 71 | self.assertEqual( 72 | a.serialize(ignore_fields=ignore_fields), 73 | b.serialize(ignore_fields=ignore_fields), 74 | "The two building blocks didn't match.", 75 | ) 76 | 77 | def assertListsEqualIgnoringFields(self, a, b, ignore_fields=None): 78 | """ 79 | The lists equals. 80 | """ 81 | ignore_fields = ignore_fields if ignore_fields is not None else [] 82 | 83 | from ddd.utils.utils import get_for_compare 84 | 85 | a = get_for_compare(a, ignore_fields) 86 | b = get_for_compare(b, ignore_fields) 87 | 88 | if not a == b: 89 | self.assertEqual( 90 | a, 91 | b, 92 | "The two lists didn't match.", 93 | ) 94 | 95 | # Helpers 96 | 97 | @classmethod 98 | def Any(cls, type_=None): 99 | """ 100 | Returns an object that can be used to compare with other objects. 101 | 102 | Example: 103 | Any(str) == some_var 104 | Any(float) == some_var 105 | """ 106 | 107 | class Any(cls): 108 | def __eq__(self, other): 109 | if type_ is None: 110 | return other is not None 111 | return type(other) == type_ 112 | 113 | return Any() 114 | -------------------------------------------------------------------------------- /ddd/tests/repositories/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/ddd/tests/repositories/__init__.py -------------------------------------------------------------------------------- /ddd/tests/repositories/memory/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/ddd/tests/repositories/memory/__init__.py -------------------------------------------------------------------------------- /ddd/tests/repositories/memory/test_memory_repository.py: -------------------------------------------------------------------------------- 1 | from ddd.tests.dummy_action_test_case import DummyActionTestCase 2 | from ddd.domain.dummy.dummy import Dummy 3 | from ddd.repositories.memory.memory_dummy_repository import \ 4 | MemoryDummyRepository 5 | 6 | 7 | class TestMemoryRepository(DummyActionTestCase): 8 | 9 | async def asyncSetUp(self): 10 | await super().asyncSetUp() 11 | 12 | self.repository = \ 13 | MemoryDummyRepository( 14 | log_service=self.deps.get_log_service(), 15 | ) 16 | 17 | Dummy.VERSION = "2" 18 | 19 | self.record_v1 = { 20 | 'id': "some-dummy-id-1", 21 | 'data': { 22 | 'version': "1", 23 | } 24 | } 25 | 26 | self.record_v2 = { 27 | 'id': "some-dummy-id-2", 28 | 'data': { 29 | 'version': "2", 30 | } 31 | } 32 | 33 | async def test_get_all_records_not_on_latest_version_returns_not_on_latest_version( 34 | self, 35 | ): 36 | # Setup 37 | await self.repository._save_record( 38 | aggregate_id=self.record_v1['id'], 39 | data=self.record_v1['data'], 40 | ) 41 | 42 | await self.repository._save_record( 43 | aggregate_id=self.record_v2['id'], 44 | data=self.record_v2['data'], 45 | ) 46 | 47 | Dummy.VERSION = "2" 48 | 49 | # Exercise 50 | records = await \ 51 | self.repository._get_all_records_not_on_latest_version() 52 | 53 | # Assert 54 | self.assertEqual( 55 | [ 56 | self.record_v1, 57 | ], 58 | records, 59 | ) 60 | -------------------------------------------------------------------------------- /ddd/tests/repositories/postgres/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/ddd/tests/repositories/postgres/__init__.py -------------------------------------------------------------------------------- /ddd/tests/repositories/postgres/test_postgres_repository.py: -------------------------------------------------------------------------------- 1 | from ddd.tests.dummy_action_test_case import DummyActionTestCase 2 | 3 | from ddd.infrastructure.db_service.memory_postgres_db_service import \ 4 | MemoryPostgresDbService 5 | 6 | from ddd.domain.dummy.dummy import Dummy 7 | from ddd.repositories.postgres.postgres_dummy_repository import \ 8 | PostgresDummyRepository 9 | 10 | 11 | class TestPostgresRepository(DummyActionTestCase): 12 | 13 | async def asyncSetUp(self): 14 | await super().asyncSetUp() 15 | 16 | self.db_service = MemoryPostgresDbService( 17 | log_service=self.deps.get_log_service(), 18 | min_size=20, 19 | max_size=20, 20 | ) 21 | 22 | self.repository = PostgresDummyRepository( 23 | config=self.config, 24 | db_service=self.db_service, 25 | log_service=self.deps.get_log_service(), 26 | ) 27 | 28 | await self.db_service.start() 29 | await self.repository.start() 30 | 31 | self.record_v1 = { 32 | 'id': "some-dummy-id-1", 33 | 'data': { 34 | 'version': "1", 35 | } 36 | } 37 | 38 | self.record_v2 = { 39 | 'id': "some-dummy-id-2", 40 | 'data': { 41 | 'version': "2", 42 | } 43 | } 44 | 45 | async def asyncTearDown(self): 46 | await self.db_service.stop() 47 | await self.repository.stop() 48 | 49 | async def test_get_all_records_not_on_latest_version_returns_not_on_latest_version( 50 | self, 51 | ): 52 | # Setup 53 | await self.repository._save_record( 54 | aggregate_id=self.record_v1['id'], 55 | data=self.record_v1['data'], 56 | ) 57 | 58 | await self.repository._save_record( 59 | aggregate_id=self.record_v2['id'], 60 | data=self.record_v2['data'], 61 | ) 62 | 63 | Dummy.VERSION = "2" 64 | 65 | # Exercise 66 | records = await \ 67 | self.repository._get_all_records_not_on_latest_version() 68 | 69 | # Assert 70 | self.assertEqual( 71 | [ 72 | self.record_v1, 73 | ], 74 | [dict(r) for r in records], 75 | ) 76 | -------------------------------------------------------------------------------- /ddd/tests/repositories/test_repository.py: -------------------------------------------------------------------------------- 1 | from addict import Dict 2 | 3 | from unittest.mock import MagicMock 4 | 5 | from ddd.domain.dummy.dummy import Dummy 6 | from ddd.repositories.postgres.postgres_dummy_repository import \ 7 | PostgresDummyRepository 8 | 9 | from ddd.tests.dummy_action_test_case import DummyActionTestCase 10 | 11 | 12 | class TestRepository(DummyActionTestCase): 13 | 14 | async def asyncSetUp(self): 15 | await super().asyncSetUp() 16 | 17 | self.repository = \ 18 | PostgresDummyRepository( 19 | config=Dict({'database': {'dsn': "some-dsn"}}), 20 | db_service=None, 21 | log_service=self.deps.get_log_service(), 22 | ) 23 | 24 | self.record_v1 = { 25 | 'id': "some-dummy-id", 26 | 'data': { 27 | 'version': "1", 28 | } 29 | } 30 | 31 | self.record_v2 = { 32 | 'id': "some-dummy-id", 33 | 'data': { 34 | 'version': "1", 35 | 'second': "value", 36 | } 37 | } 38 | 39 | self.record_v3 = { 40 | 'id': "some-dummy-id", 41 | 'data': { 42 | 'version': "1", 43 | 'second': "value", 44 | 'third': "value", 45 | } 46 | } 47 | 48 | async def test_get_versions_to_migrate_to_returns_unmigrated_versions_when_not_on_latest_version( 49 | self, 50 | ): 51 | # Setup 52 | Dummy.VERSION = "3" 53 | 54 | # Exercise 55 | versions = \ 56 | self.repository._get_versions_to_migrate_to( 57 | record=self.record_v1, 58 | cls=Dummy, 59 | ) 60 | 61 | # Assert 62 | self.assertEqual( 63 | range(2, 3), 64 | versions, 65 | ) 66 | 67 | async def test_get_versions_to_migrate_to_returns_no_range_when_on_latest_version( 68 | self, 69 | ): 70 | # Setup 71 | Dummy.VERSION = "1" 72 | 73 | # Exercise 74 | versions = \ 75 | self.repository._get_versions_to_migrate_to( 76 | record=self.record_v1, 77 | cls=Dummy, 78 | ) 79 | 80 | # Assert 81 | self.assertEqual( 82 | range(0, 0), 83 | versions, 84 | ) 85 | 86 | async def test_migrate_calls_all_migration_funcs( 87 | self, 88 | ): 89 | # Setup 90 | Dummy.VERSION = "3" 91 | 92 | # ..mock 93 | mock_migrate_v1_to_v2 = MagicMock( 94 | return_value=self.record_v2, 95 | ) 96 | 97 | mock_migrate_v2_to_v3 = MagicMock( 98 | return_value=self.record_v3, 99 | ) 100 | 101 | self.repository._migrate_v1_to_v2 = mock_migrate_v1_to_v2 102 | self.repository._migrate_v2_to_v3 = mock_migrate_v2_to_v3 103 | 104 | # Exercise 105 | self.repository._migrate( 106 | records=[self.record_v1], 107 | cls=Dummy, 108 | ) 109 | 110 | # Assert 111 | mock_migrate_v1_to_v2.assert_called_with( 112 | record=self.record_v1, 113 | ) 114 | 115 | mock_migrate_v2_to_v3.assert_called_with( 116 | record=self.record_v2, 117 | ) 118 | -------------------------------------------------------------------------------- /ddd/tests/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/ddd/tests/utils/__init__.py -------------------------------------------------------------------------------- /ddd/tests/utils/tasks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/ddd/tests/utils/tasks/__init__.py -------------------------------------------------------------------------------- /ddd/tests/utils/tasks/test_migrate_models.py: -------------------------------------------------------------------------------- 1 | from ddd.tests.dummy_action_test_case import DummyActionTestCase 2 | from ddd.utils.tasks.migrate_models import Task as MigrateModelsTask 3 | from ddd.domain.dummy.dummy import Dummy 4 | 5 | 6 | class TestMigrateModels(DummyActionTestCase): 7 | 8 | async def asyncSetUp(self): 9 | await super().asyncSetUp() 10 | 11 | async def test_records_migrated_to_latest_version_when_running( 12 | self, 13 | ): 14 | # Setup 15 | Dummy.VERSION = "2" 16 | 17 | aggregate_id = "some-dummy-id" 18 | 19 | data = { 20 | 'version': "1", 21 | 'dummy_id': aggregate_id, 22 | } 23 | 24 | repository = self.deps.get_dummy_repository() 25 | 26 | await repository._save_record( 27 | aggregate_id=aggregate_id, 28 | data=data, 29 | ) 30 | 31 | command = \ 32 | MigrateModelsTask( 33 | config=self.config, 34 | deps_mgr=self.deps, 35 | args_str="", 36 | ) 37 | 38 | # Exercise 39 | await command._run() 40 | 41 | record = \ 42 | await repository._get_record( 43 | aggregate_id=aggregate_id, 44 | ) 45 | 46 | # Assert 47 | self.assertEqual( 48 | { 49 | 'id': aggregate_id, 50 | 'data': { 51 | 'version': "2", 52 | 'dummy_id': aggregate_id, 53 | 'property_added': 'in_version_2', 54 | }, 55 | }, 56 | dict(record), 57 | ) 58 | -------------------------------------------------------------------------------- /ddd/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/ddd/utils/__init__.py -------------------------------------------------------------------------------- /ddd/utils/tasks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/ddd/utils/tasks/__init__.py -------------------------------------------------------------------------------- /ddd/utils/tasks/task.py: -------------------------------------------------------------------------------- 1 | import aiosonic 2 | import argparse 3 | 4 | from abc import ABCMeta, abstractmethod 5 | 6 | 7 | class Task(object, metaclass=ABCMeta): 8 | """ 9 | A base class for tasks. 10 | """ 11 | def __init__(self, config, deps_mgr, args_str, makes_requests=False): 12 | super().__init__() 13 | 14 | self.config = config 15 | self.deps_mgr = deps_mgr 16 | self.args_str = args_str 17 | self.makes_requests = makes_requests 18 | 19 | self.log_service = deps_mgr.get_log_service() 20 | 21 | self._create_args_parser() 22 | self.add_args(parser=self.parser) 23 | self._parse_args() 24 | 25 | self.client = None 26 | self.token = None 27 | self.pool = None 28 | 29 | # Interface 30 | 31 | @abstractmethod 32 | def add_args(self, parser): 33 | """ 34 | Override this method in subclasses. 35 | """ 36 | pass 37 | 38 | @abstractmethod 39 | async def run(self): 40 | """ 41 | Override this method in subclasses. 42 | """ 43 | pass 44 | 45 | # Init 46 | 47 | def _create_args_parser(self): 48 | self.parser = argparse.ArgumentParser() 49 | 50 | def _parse_args(self): 51 | args = [] 52 | 53 | if len(self.args_str) > 0: 54 | args = self.args_str.split(" ") 55 | 56 | self.args = self.parser.parse_args(args=args) 57 | 58 | # Run 59 | 60 | async def _run(self): 61 | if "-h" in self.args or "--help" in self.args: 62 | self.parser.print_help() 63 | else: 64 | await self.initialize() 65 | await self.run() 66 | 67 | async def initialize(self): 68 | """ 69 | Override this method if you need to do some initialization 70 | in your subclass before run() is called, 71 | (for example initiate state variables). 72 | """ 73 | # Adapters 74 | await self._create_secondary_adapters() 75 | 76 | # Client 77 | if self.makes_requests: 78 | self.client = aiosonic.HTTPClient() 79 | await self._login() 80 | 81 | # HTTP 82 | 83 | async def _create_secondary_adapters(self): 84 | """ 85 | Create secondary adapters. 86 | """ 87 | # Scheduler adapter 88 | self.scheduler_adapter = self.deps_mgr.get_scheduler_adapter() 89 | 90 | await self.scheduler_adapter.start() 91 | 92 | async def _login(self): 93 | raise NotImplementedError() 94 | 95 | # Helpers 96 | 97 | def _non_empty_string(self, string): 98 | """ 99 | Custom type for argparse 100 | """ 101 | if not string: 102 | raise ValueError("Must not be empty string") 103 | return string 104 | -------------------------------------------------------------------------------- /development/Makefile: -------------------------------------------------------------------------------- 1 | ########################################################################## 2 | # This is the development environment's Makefile. 3 | ########################################################################## 4 | 5 | ########################################################################## 6 | # VARIABLES 7 | ########################################################################## 8 | 9 | PWD = $(shell pwd) 10 | HOME := $(shell echo ~) 11 | POSTGRES_DIR = $(shell pwd)/postgres 12 | POSTGRES_DATA_DIR = $(shell pwd)/postgres/data 13 | 14 | # Load from env file 15 | include env 16 | export $(shell sed 's/=.*//' env) 17 | 18 | ########################################################################## 19 | # MENU 20 | ########################################################################## 21 | 22 | .PHONY: help 23 | help: 24 | @awk 'BEGIN {FS = ":.*?## "} /^[0-9a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) 25 | 26 | ################################################################################ 27 | # DATABASE 28 | ################################################################################ 29 | 30 | .PHONY: postgres-stop 31 | postgres-stop: ## stop postgres 32 | @docker container stop ddd-postgres || true 33 | @docker container rm ddd-postgres || true 34 | 35 | .PHONY: postgres-deploy 36 | postgres-deploy:: postgres-stop ## deploy postgres 37 | @docker run \ 38 | --restart unless-stopped \ 39 | --name ddd-postgres \ 40 | --network $(NETWORK) \ 41 | --log-opt max-size=$(LOGS_MAX_SIZE) \ 42 | --log-opt max-file=$(LOGS_MAX_FILE) \ 43 | -p "5434:5432" \ 44 | -e "POSTGRES_PASSWORD=$(POSTGRES_PASSWORD)" \ 45 | -v $(POSTGRES_DATA_DIR):/var/lib/postgresql/data \ 46 | -v $(POSTGRES_DIR)/postgresql.conf:/etc/postgresql/postgresql.conf \ 47 | -d \ 48 | postgres:10 -c 'config_file=/etc/postgresql/postgresql.conf' 49 | 50 | .PHONY: postgres-setup 51 | postgres-setup:: ## setup the database (for tests) 52 | docker exec -t ddd-postgres psql -U postgres -c <<< echo "create database test;" 53 | docker exec -t ddd-postgres psql -U postgres -c <<< echo "create user test with encrypted password '$(POSTGRES_TEST_PASSWORD)';" 54 | docker exec -t ddd-postgres psql -U postgres -c <<< echo "grant all privileges on database test to test;" 55 | -------------------------------------------------------------------------------- /development/env.sample: -------------------------------------------------------------------------------- 1 | NETWORK=ddd 2 | 3 | LOGS_MAX_SIZE=10m 4 | LOGS_MAX_FILE=5 5 | 6 | POSTGRES_PASSWORD= 7 | POSTGRES_TEST_PASSWORD= 8 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_build/doctrees/community.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/doctrees/community.doctree -------------------------------------------------------------------------------- /docs/_build/doctrees/environment.pickle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/doctrees/environment.pickle -------------------------------------------------------------------------------- /docs/_build/doctrees/gettingstarted.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/doctrees/gettingstarted.doctree -------------------------------------------------------------------------------- /docs/_build/doctrees/index.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/doctrees/index.doctree -------------------------------------------------------------------------------- /docs/_build/doctrees/modules/adapters/adapter.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/doctrees/modules/adapters/adapter.doctree -------------------------------------------------------------------------------- /docs/_build/doctrees/modules/application/action.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/doctrees/modules/application/action.doctree -------------------------------------------------------------------------------- /docs/_build/doctrees/modules/application/application_service.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/doctrees/modules/application/application_service.doctree -------------------------------------------------------------------------------- /docs/_build/doctrees/modules/application/config.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/doctrees/modules/application/config.doctree -------------------------------------------------------------------------------- /docs/_build/doctrees/modules/domain/event/domain_event.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/doctrees/modules/domain/event/domain_event.doctree -------------------------------------------------------------------------------- /docs/_build/doctrees/modules/domain/event/integration_event.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/doctrees/modules/domain/event/integration_event.doctree -------------------------------------------------------------------------------- /docs/_build/doctrees/modules/infrastructure/db_service/db_service.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/doctrees/modules/infrastructure/db_service/db_service.doctree -------------------------------------------------------------------------------- /docs/_build/doctrees/modules/infrastructure/db_service/memory_db_service.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/doctrees/modules/infrastructure/db_service/memory_db_service.doctree -------------------------------------------------------------------------------- /docs/_build/doctrees/modules/infrastructure/db_service/memory_postgres_db_service.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/doctrees/modules/infrastructure/db_service/memory_postgres_db_service.doctree -------------------------------------------------------------------------------- /docs/_build/doctrees/modules/infrastructure/db_service/postgres_db_service.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/doctrees/modules/infrastructure/db_service/postgres_db_service.doctree -------------------------------------------------------------------------------- /docs/_build/doctrees/modules/utils/dep_mgr/dep_mgr.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/doctrees/modules/utils/dep_mgr/dep_mgr.doctree -------------------------------------------------------------------------------- /docs/_build/doctrees/modules/utils/tasks/task/task.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/doctrees/modules/utils/tasks/task/task.doctree -------------------------------------------------------------------------------- /docs/_build/doctrees/py-modindex.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/doctrees/py-modindex.doctree -------------------------------------------------------------------------------- /docs/_build/doctrees/versionhistory.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/doctrees/versionhistory.doctree -------------------------------------------------------------------------------- /docs/_build/html/.buildinfo: -------------------------------------------------------------------------------- 1 | # Sphinx build info version 1 2 | # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. 3 | config: 79004c452e75b1289f6ad6937f188c4a 4 | tags: 645f666f9bcd5a90fca523b33c5a78b7 5 | -------------------------------------------------------------------------------- /docs/_build/html/.doctrees/community.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/html/.doctrees/community.doctree -------------------------------------------------------------------------------- /docs/_build/html/.doctrees/environment.pickle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/html/.doctrees/environment.pickle -------------------------------------------------------------------------------- /docs/_build/html/.doctrees/gettingstarted.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/html/.doctrees/gettingstarted.doctree -------------------------------------------------------------------------------- /docs/_build/html/.doctrees/index.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/html/.doctrees/index.doctree -------------------------------------------------------------------------------- /docs/_build/html/.doctrees/modules/adapters/adapter.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/html/.doctrees/modules/adapters/adapter.doctree -------------------------------------------------------------------------------- /docs/_build/html/.doctrees/modules/application/action.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/html/.doctrees/modules/application/action.doctree -------------------------------------------------------------------------------- /docs/_build/html/.doctrees/modules/application/application_service.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/html/.doctrees/modules/application/application_service.doctree -------------------------------------------------------------------------------- /docs/_build/html/.doctrees/modules/application/config.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/html/.doctrees/modules/application/config.doctree -------------------------------------------------------------------------------- /docs/_build/html/.doctrees/modules/domain/event/domain_event.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/html/.doctrees/modules/domain/event/domain_event.doctree -------------------------------------------------------------------------------- /docs/_build/html/.doctrees/modules/domain/event/integration_event.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/html/.doctrees/modules/domain/event/integration_event.doctree -------------------------------------------------------------------------------- /docs/_build/html/.doctrees/modules/infrastructure/db_service/db_service.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/html/.doctrees/modules/infrastructure/db_service/db_service.doctree -------------------------------------------------------------------------------- /docs/_build/html/.doctrees/modules/infrastructure/db_service/memory_db_service.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/html/.doctrees/modules/infrastructure/db_service/memory_db_service.doctree -------------------------------------------------------------------------------- /docs/_build/html/.doctrees/modules/infrastructure/db_service/memory_postgres_db_service.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/html/.doctrees/modules/infrastructure/db_service/memory_postgres_db_service.doctree -------------------------------------------------------------------------------- /docs/_build/html/.doctrees/modules/infrastructure/db_service/postgres_db_service.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/html/.doctrees/modules/infrastructure/db_service/postgres_db_service.doctree -------------------------------------------------------------------------------- /docs/_build/html/.doctrees/modules/utils/dep_mgr/dep_mgr.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/html/.doctrees/modules/utils/dep_mgr/dep_mgr.doctree -------------------------------------------------------------------------------- /docs/_build/html/.doctrees/modules/utils/tasks/task/task.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/html/.doctrees/modules/utils/tasks/task/task.doctree -------------------------------------------------------------------------------- /docs/_build/html/.doctrees/py-modindex.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/html/.doctrees/py-modindex.doctree -------------------------------------------------------------------------------- /docs/_build/html/.doctrees/versionhistory.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/html/.doctrees/versionhistory.doctree -------------------------------------------------------------------------------- /docs/_build/html/_sources/community.rst.txt: -------------------------------------------------------------------------------- 1 | .. community: 2 | 3 | ############ 4 | Contribution 5 | ############ 6 | 7 | You are welcome to suggest changes and to submit bug reports at the github repository. 8 | 9 | We also look forward to seeing user guides as this project is being adopted. 10 | -------------------------------------------------------------------------------- /docs/_build/html/_sources/index.rst.txt: -------------------------------------------------------------------------------- 1 | ddd-for-python 2 | ============ 3 | 4 | Welcome to the ddd-for-python framework. 5 | 6 | This framework is used to do DDD with python. Below is an example of a bounded context implemented with ddd-for-python. 7 | 8 | Check out the :doc:`user guide` to get started building your own contexts. 9 | 10 | 11 | Example 12 | ======= 13 | 14 | .. code-block:: python 15 | 16 | from ddd.application.config import Config 17 | from ddd.infrastructure.container import Container 18 | 19 | from shipping.utils.dep_mgr import DependencyManager 20 | from shipping.application.shipping_application_service import \ 21 | ShippingApplicationService 22 | 23 | 24 | if __name__ == "__main__": 25 | 26 | # Config 27 | config = Config() 28 | 29 | # Dependency manager 30 | dep_mgr = \ 31 | DependencyManager( 32 | config=config, 33 | ) 34 | 35 | # Application service 36 | service = \ 37 | ShippingApplicationService( 38 | customer_repository=dep_mgr.get_customer_repository(), 39 | db_service=dep_mgr.get_db_service(), 40 | domain_adapter=dep_mgr.get_domain_adapter(), 41 | domain_publisher=dep_mgr.get_domain_publisher(), 42 | event_repository=dep_mgr.get_event_repository(), 43 | interchange_adapter=dep_mgr.get_interchange_adapter(), 44 | interchange_publisher=dep_mgr.get_interchange_publisher(), 45 | job_adapter=dep_mgr.get_job_adapter(), 46 | job_service=dep_mgr.get_job_service(), 47 | log_service=dep_mgr.get_log_service(), 48 | scheduler_adapter=dep_mgr.get_scheduler_adapter(), 49 | shipment_repository=dep_mgr.get_shipment_repository(), 50 | max_concurrent_actions=config.max_concurrent_actions, 51 | loop=config.loop.instance, 52 | ) 53 | 54 | # ..register 55 | dep_mgr.set_service(service) 56 | 57 | # Container 58 | container = \ 59 | Container( 60 | app_service=service, 61 | log_service=dep_mgr.get_log_service(), 62 | ) 63 | 64 | # ..run 65 | loop = config.loop.instance 66 | loop.run_until_complete(container.run()) 67 | loop.close() 68 | 69 | .. gettingstarted-docs: 70 | .. toctree:: 71 | :maxdepth: 1 72 | :caption: User guide 73 | 74 | gettingstarted 75 | 76 | .. versionhistory-docs: 77 | .. toctree:: 78 | :maxdepth: 1 79 | :caption: Releases 80 | 81 | versionhistory 82 | 83 | .. troubleshooting-docs: 84 | .. toctree:: 85 | :maxdepth: 1 86 | :caption: Troubleshooting 87 | 88 | troubleshooting 89 | 90 | .. community-docs: 91 | .. toctree:: 92 | :maxdepth: 1 93 | :caption: Community 94 | 95 | community 96 | 97 | .. apireference-docs: 98 | .. toctree:: 99 | :maxdepth: 1 100 | :caption: API Reference 101 | 102 | py-modindex 103 | -------------------------------------------------------------------------------- /docs/_build/html/_sources/modules/adapters/adapter.rst.txt: -------------------------------------------------------------------------------- 1 | :mod:`ddd.adapters.adapter` 2 | ========================================== 3 | 4 | .. automodule:: ddd.adapters.adapter 5 | 6 | ### 7 | API 8 | ### 9 | 10 | .. autoclass:: Adapter 11 | :members: 12 | -------------------------------------------------------------------------------- /docs/_build/html/_sources/modules/application/action.rst.txt: -------------------------------------------------------------------------------- 1 | :mod:`ddd.application.action` 2 | ========================================== 3 | 4 | The :class:`~ddd.application.application_service.ApplicationService` contains one or more functions that implements the actions. 5 | 6 | .. automodule:: ddd.application.action 7 | 8 | API 9 | --- 10 | 11 | .. autofunction:: action 12 | 13 | 14 | Examples 15 | -------- 16 | 17 | Decorate a function in the application service:: 18 | 19 | @action 20 | async def send_tracking_email(self, command, corr_ids=None): 21 | """ 22 | Send tracking email to recipient. 23 | """ 24 | ... 25 | -------------------------------------------------------------------------------- /docs/_build/html/_sources/modules/application/application_service.rst.txt: -------------------------------------------------------------------------------- 1 | :mod:`ddd.application.application_service` 2 | ========================================== 3 | 4 | .. automodule:: ddd.application.application_service 5 | 6 | API 7 | --- 8 | 9 | .. autoclass:: ApplicationService 10 | :members: 11 | -------------------------------------------------------------------------------- /docs/_build/html/_sources/modules/application/config.rst.txt: -------------------------------------------------------------------------------- 1 | :mod:`ddd.application.config` 2 | ==================================== 3 | 4 | .. automodule:: ddd.application.config 5 | 6 | API 7 | --- 8 | 9 | .. autoclass:: Config 10 | :members: _declare_settings, _read_config 11 | -------------------------------------------------------------------------------- /docs/_build/html/_sources/modules/domain/event/domain_event.rst.txt: -------------------------------------------------------------------------------- 1 | :mod:`ddd.domain.event.domain_event` 2 | ========================================== 3 | 4 | .. automodule:: ddd.domain.event.domain_event 5 | 6 | API 7 | --- 8 | 9 | .. autoclass:: DomainEvent 10 | :members: get_serialized_payload 11 | :show-inheritance: 12 | 13 | 14 | Examples 15 | -------- 16 | 17 | TODO: ... 18 | -------------------------------------------------------------------------------- /docs/_build/html/_sources/modules/domain/event/integration_event.rst.txt: -------------------------------------------------------------------------------- 1 | :mod:`ddd.domain.event.integration_event` 2 | ========================================== 3 | 4 | .. automodule:: ddd.domain.event.integration_event 5 | 6 | API 7 | --- 8 | 9 | .. autoclass:: IntegrationEvent 10 | :members: get_serialized_payload 11 | :show-inheritance: 12 | 13 | 14 | Examples 15 | -------- 16 | 17 | TODO: ... 18 | -------------------------------------------------------------------------------- /docs/_build/html/_sources/modules/infrastructure/db_service/db_service.rst.txt: -------------------------------------------------------------------------------- 1 | :mod:`ddd.infrastructure.db_service` 2 | ==================================== 3 | 4 | The db service is used to acquire connections to the database. 5 | 6 | You will not use this base class directly. Instead, you use one of the implementations: 7 | :class:`~ddd.infrastructure.db_service.postgres_db_service.PostgresDbService` 8 | :class:`~ddd.infrastructure.db_service.memory_db_service.MemoryDbService` or 9 | :class:`~ddd.infrastructure.db_service.memory_postgres_db_service.MemoryPostgresDbService`. 10 | 11 | Look at the documentation to figure out which one to use. 12 | 13 | 14 | .. automodule:: ddd.infrastructure.db_service.db_service 15 | 16 | API 17 | --- 18 | 19 | .. autoclass:: DbService 20 | :members: 21 | -------------------------------------------------------------------------------- /docs/_build/html/_sources/modules/infrastructure/db_service/memory_db_service.rst.txt: -------------------------------------------------------------------------------- 1 | :mod:`ddd.infrastructure.db_service.memory_db_service` 2 | ====================================================== 3 | 4 | .. automodule:: ddd.infrastructure.db_service.memory_db_service 5 | 6 | API 7 | --- 8 | 9 | .. autoclass:: MemoryDbService 10 | :members: 11 | :show-inheritance: 12 | 13 | 14 | Examples 15 | -------- 16 | 17 | Create a ``memory_db_service`` and ``start`` it:: 18 | 19 | from ddd.infrastructure.db_service.memory_db_service import MemoryDbService 20 | 21 | 22 | log_service = ... 23 | 24 | db_service = \ 25 | MemoryDbService( 26 | log_service=log_service, 27 | ) 28 | 29 | await db_service.start() 30 | -------------------------------------------------------------------------------- /docs/_build/html/_sources/modules/infrastructure/db_service/memory_postgres_db_service.rst.txt: -------------------------------------------------------------------------------- 1 | :mod:`ddd.infrastructure.db_service.memory_postgres_db_service` 2 | =============================================================== 3 | 4 | .. automodule:: ddd.infrastructure.db_service.memory_postgres_db_service 5 | 6 | API 7 | --- 8 | 9 | .. autoclass:: MemoryPostgresDbService 10 | :members: 11 | :show-inheritance: 12 | 13 | 14 | Examples 15 | -------- 16 | 17 | Create a ``memory_postgres_db_service`` and ``start`` it:: 18 | 19 | from ddd.infrastructure.db_service.memory_postgres_db_service import MemoryPostgresDbService 20 | 21 | 22 | log_service = ... 23 | 24 | db_service = \ 25 | MemoryPostgresDbService( 26 | log_service=log_service, 27 | min_size=20, 28 | max_size=20, 29 | ) 30 | 31 | await db_service.start() 32 | -------------------------------------------------------------------------------- /docs/_build/html/_sources/modules/infrastructure/db_service/postgres_db_service.rst.txt: -------------------------------------------------------------------------------- 1 | :mod:`ddd.infrastructure.db_service.postgres_db_service` 2 | ======================================================== 3 | 4 | .. automodule:: ddd.infrastructure.db_service.postgres_db_service 5 | 6 | API 7 | --- 8 | 9 | .. autoclass:: PostgresDbService 10 | :members: start, stop 11 | :show-inheritance: 12 | 13 | 14 | Examples 15 | -------- 16 | 17 | Create a ``postgres_db_service`` and ``start`` it:: 18 | 19 | from ddd.infrastructure.db_service.postgres_db_service import PostgresDbService 20 | 21 | 22 | log_service = ... 23 | 24 | db_service = \ 25 | PostgresDbService( 26 | dsn="postgresql://localhost:5432", 27 | log_service=log_service, 28 | min_size=20, 29 | max_size=20, 30 | ) 31 | 32 | await db_service.start() 33 | -------------------------------------------------------------------------------- /docs/_build/html/_sources/modules/utils/dep_mgr/dep_mgr.rst.txt: -------------------------------------------------------------------------------- 1 | :mod:`ddd.utils.dep_mgr` 2 | ==================================== 3 | 4 | .. automodule:: ddd.utils.dep_mgr 5 | 6 | API 7 | --- 8 | 9 | .. autoclass:: DependencyManager 10 | :members: 11 | -------------------------------------------------------------------------------- /docs/_build/html/_sources/modules/utils/tasks/task/task.rst.txt: -------------------------------------------------------------------------------- 1 | :mod:`ddd.utils.tasks.task` 2 | ==================================== 3 | 4 | .. automodule:: ddd.utils.tasks.task 5 | 6 | API 7 | --- 8 | 9 | .. autoclass:: Task 10 | :members: 11 | -------------------------------------------------------------------------------- /docs/_build/html/_sources/py-modindex.rst.txt: -------------------------------------------------------------------------------- 1 | API reference 2 | ============= 3 | 4 | -------------------------------------------------------------------------------- /docs/_build/html/_sources/versionhistory.rst.txt: -------------------------------------------------------------------------------- 1 | ############### 2 | Version history 3 | ############### 4 | 5 | **0.9.5** 6 | 7 | - Added documentation. 8 | - Moved db_service related classes. 9 | - Moved event related classes. 10 | - Added :class:`~ddd.infrastructure.db_service.memory_postgres_db_service.MemoryPostgresDbService` to be able to run tests against an in-memory postgres database. 11 | - Fixed bug: container kwarg in example main.py (thanks euri10). 12 | 13 | .. _documentation: https://ddd-for-python.readthedocs.io/en/latest/ 14 | 15 | **0.9.4** 16 | 17 | - Added ``context`` to log service's log messages. 18 | - Moveed record filtering methods to base repository class. 19 | - Added ``uses_service`` to Task class. Deprecate ``makes_requests``. 20 | 21 | **0.9.3** 22 | 23 | - Searching env file from cwd by default in tests, (when no path specified). 24 | - Refactored Task class to make it more simple. 25 | - Refactored the configuration solution by adding a Config class. 26 | - Added example code for ``shipping`` context of a webshop application. 27 | - Added get_all_jobs() and get_job_count() to scheduler adapter & service. 28 | - Added missing call to _migrate() in a couple of Repository class functions. 29 | 30 | **0.9.2** 31 | 32 | - Fixed bug: Env file wasn't loaded in certain circumstances. 33 | 34 | **0.9.1** 35 | 36 | - Initial commit. 37 | -------------------------------------------------------------------------------- /docs/_build/html/_static/css/badge_only.css: -------------------------------------------------------------------------------- 1 | .fa:before{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-style:normal;font-weight:400;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#FontAwesome) format("svg")}.fa:before{font-family:FontAwesome;font-style:normal;font-weight:400;line-height:1}.fa:before,a .fa{text-decoration:inherit}.fa:before,a .fa,li .fa{display:inline-block}li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-.8em}ul.fas li .fa{width:.8em}ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before,.icon-book:before{content:"\f02d"}.fa-caret-down:before,.icon-caret-down:before{content:"\f0d7"}.fa-caret-up:before,.icon-caret-up:before{content:"\f0d8"}.fa-caret-left:before,.icon-caret-left:before{content:"\f0d9"}.fa-caret-right:before,.icon-caret-right:before{content:"\f0da"}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60}.rst-versions .rst-current-version:after{clear:both;content:"";display:block}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}} -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/Roboto-Slab-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/html/_static/css/fonts/Roboto-Slab-Bold.woff -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/Roboto-Slab-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/html/_static/css/fonts/Roboto-Slab-Bold.woff2 -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/Roboto-Slab-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/html/_static/css/fonts/Roboto-Slab-Regular.woff -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/Roboto-Slab-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/html/_static/css/fonts/Roboto-Slab-Regular.woff2 -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/html/_static/css/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/html/_static/css/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/html/_static/css/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/html/_static/css/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/lato-bold-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/html/_static/css/fonts/lato-bold-italic.woff -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/lato-bold-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/html/_static/css/fonts/lato-bold-italic.woff2 -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/lato-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/html/_static/css/fonts/lato-bold.woff -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/lato-bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/html/_static/css/fonts/lato-bold.woff2 -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/lato-normal-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/html/_static/css/fonts/lato-normal-italic.woff -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/lato-normal-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/html/_static/css/fonts/lato-normal-italic.woff2 -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/lato-normal.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/html/_static/css/fonts/lato-normal.woff -------------------------------------------------------------------------------- /docs/_build/html/_static/css/fonts/lato-normal.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/html/_static/css/fonts/lato-normal.woff2 -------------------------------------------------------------------------------- /docs/_build/html/_static/documentation_options.js: -------------------------------------------------------------------------------- 1 | var DOCUMENTATION_OPTIONS = { 2 | URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'), 3 | VERSION: '0.9.5', 4 | LANGUAGE: 'None', 5 | COLLAPSE_INDEX: false, 6 | BUILDER: 'html', 7 | FILE_SUFFIX: '.html', 8 | LINK_SUFFIX: '.html', 9 | HAS_SOURCE: true, 10 | SOURCELINK_SUFFIX: '.txt', 11 | NAVIGATION_WITH_KEYS: false 12 | }; -------------------------------------------------------------------------------- /docs/_build/html/_static/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/html/_static/file.png -------------------------------------------------------------------------------- /docs/_build/html/_static/js/badge_only.js: -------------------------------------------------------------------------------- 1 | !function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=4)}({4:function(e,t,r){}}); -------------------------------------------------------------------------------- /docs/_build/html/_static/js/html5shiv.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @preserve HTML5 Shiv 3.7.3 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed 3 | */ 4 | !function(a,b){function c(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function d(){var a=t.elements;return"string"==typeof a?a.split(" "):a}function e(a,b){var c=t.elements;"string"!=typeof c&&(c=c.join(" ")),"string"!=typeof a&&(a=a.join(" ")),t.elements=c+" "+a,j(b)}function f(a){var b=s[a[q]];return b||(b={},r++,a[q]=r,s[r]=b),b}function g(a,c,d){if(c||(c=b),l)return c.createElement(a);d||(d=f(c));var e;return e=d.cache[a]?d.cache[a].cloneNode():p.test(a)?(d.cache[a]=d.createElem(a)).cloneNode():d.createElem(a),!e.canHaveChildren||o.test(a)||e.tagUrn?e:d.frag.appendChild(e)}function h(a,c){if(a||(a=b),l)return a.createDocumentFragment();c=c||f(a);for(var e=c.frag.cloneNode(),g=0,h=d(),i=h.length;i>g;g++)e.createElement(h[g]);return e}function i(a,b){b.cache||(b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag()),a.createElement=function(c){return t.shivMethods?g(c,a,b):b.createElem(c)},a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+d().join().replace(/[\w\-:]+/g,function(a){return b.createElem(a),b.frag.createElement(a),'c("'+a+'")'})+");return n}")(t,b.frag)}function j(a){a||(a=b);var d=f(a);return!t.shivCSS||k||d.hasCSS||(d.hasCSS=!!c(a,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),l||i(a,d),a}var k,l,m="3.7.3-pre",n=a.html5||{},o=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,p=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,q="_html5shiv",r=0,s={};!function(){try{var a=b.createElement("a");a.innerHTML="",k="hidden"in a,l=1==a.childNodes.length||function(){b.createElement("a");var a=b.createDocumentFragment();return"undefined"==typeof a.cloneNode||"undefined"==typeof a.createDocumentFragment||"undefined"==typeof a.createElement}()}catch(c){k=!0,l=!0}}();var t={elements:n.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:m,shivCSS:n.shivCSS!==!1,supportsUnknownElements:l,shivMethods:n.shivMethods!==!1,type:"default",shivDocument:j,createElement:g,createDocumentFragment:h,addElements:e};a.html5=t,j(b),"object"==typeof module&&module.exports&&(module.exports=t)}("undefined"!=typeof window?window:this,document); -------------------------------------------------------------------------------- /docs/_build/html/_static/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/html/_static/minus.png -------------------------------------------------------------------------------- /docs/_build/html/_static/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/html/_static/plus.png -------------------------------------------------------------------------------- /docs/_build/html/objects.inv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/_build/html/objects.inv -------------------------------------------------------------------------------- /docs/community.rst: -------------------------------------------------------------------------------- 1 | .. community: 2 | 3 | ############ 4 | Contribution 5 | ############ 6 | 7 | You are welcome to suggest changes and to submit bug reports at the github repository. 8 | 9 | We also look forward to seeing user guides as this project is being adopted. 10 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | sys.path.insert(0, os.path.abspath('../')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'ddd-for-python' 21 | copyright = '2021, David Runemalm' 22 | author = 'David Runemalm' 23 | 24 | # The full version, including alpha/beta/rc tags 25 | release = '0.9.5' 26 | 27 | 28 | # -- General configuration --------------------------------------------------- 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'sphinx.ext.autodoc', 35 | 'sphinx.ext.intersphinx', 36 | 'sphinx.ext.autosectionlabel', 37 | 'sphinx_rtd_theme', 38 | ] 39 | 40 | # List of directories, relative to source directory, that shouldn't be searched 41 | # for source files. 42 | exclude_trees = ['build', '.git', 'examples'] 43 | 44 | # Add any paths that contain templates here, relative to this directory. 45 | templates_path = ['_templates'] 46 | 47 | # List of patterns, relative to source directory, that match files and 48 | # directories to ignore when looking for source files. 49 | # This pattern also affects html_static_path and html_extra_path. 50 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 51 | 52 | # The name of the Pygments (syntax highlighting) style to use. 53 | pygments_style = 'sphinx' 54 | 55 | # A list of ignored prefixes for module index sorting. 56 | #modindex_common_prefix = [] 57 | 58 | autodoc_member_order = 'alphabetical' 59 | 60 | # -- Options for HTML output ------------------------------------------------- 61 | 62 | # The theme to use for HTML and HTML Help pages. See the documentation for 63 | # a list of builtin themes. 64 | # 65 | html_theme = 'sphinx_rtd_theme' 66 | 67 | # Add any paths that contain custom static files (such as style sheets) here, 68 | # relative to this directory. They are copied after the builtin static files, 69 | # so a file named "default.css" will overwrite the builtin "default.css". 70 | html_static_path = ['_static'] 71 | 72 | intersphinx_mapping = {'python': ('https://docs.python.org/', None), 73 | 'sqlalchemy': ('http://docs.sqlalchemy.org/en/latest/', None)} 74 | -------------------------------------------------------------------------------- /docs/images/folderstructure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/docs/images/folderstructure.png -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ddd-for-python 2 | ============ 3 | 4 | Welcome to the ddd-for-python framework. 5 | 6 | This framework is used to do DDD with python. Below is an example of a bounded context implemented with ddd-for-python. 7 | 8 | Check out the :doc:`user guide` to get started building your own contexts. 9 | 10 | 11 | Example 12 | ======= 13 | 14 | .. code-block:: python 15 | 16 | from ddd.application.config import Config 17 | from ddd.infrastructure.container import Container 18 | 19 | from shipping.utils.dep_mgr import DependencyManager 20 | from shipping.application.shipping_application_service import \ 21 | ShippingApplicationService 22 | 23 | 24 | if __name__ == "__main__": 25 | 26 | # Config 27 | config = Config() 28 | 29 | # Dependency manager 30 | dep_mgr = \ 31 | DependencyManager( 32 | config=config, 33 | ) 34 | 35 | # Application service 36 | service = \ 37 | ShippingApplicationService( 38 | customer_repository=dep_mgr.get_customer_repository(), 39 | db_service=dep_mgr.get_db_service(), 40 | domain_adapter=dep_mgr.get_domain_adapter(), 41 | domain_publisher=dep_mgr.get_domain_publisher(), 42 | event_repository=dep_mgr.get_event_repository(), 43 | interchange_adapter=dep_mgr.get_interchange_adapter(), 44 | interchange_publisher=dep_mgr.get_interchange_publisher(), 45 | job_adapter=dep_mgr.get_job_adapter(), 46 | job_service=dep_mgr.get_job_service(), 47 | log_service=dep_mgr.get_log_service(), 48 | scheduler_adapter=dep_mgr.get_scheduler_adapter(), 49 | shipment_repository=dep_mgr.get_shipment_repository(), 50 | max_concurrent_actions=config.max_concurrent_actions, 51 | loop=config.loop.instance, 52 | ) 53 | 54 | # ..register 55 | dep_mgr.set_service(service) 56 | 57 | # Container 58 | container = \ 59 | Container( 60 | app_service=service, 61 | log_service=dep_mgr.get_log_service(), 62 | ) 63 | 64 | # ..run 65 | loop = config.loop.instance 66 | loop.run_until_complete(container.run()) 67 | loop.close() 68 | 69 | .. gettingstarted-docs: 70 | .. toctree:: 71 | :maxdepth: 1 72 | :caption: User guide 73 | 74 | gettingstarted 75 | 76 | .. versionhistory-docs: 77 | .. toctree:: 78 | :maxdepth: 1 79 | :caption: Releases 80 | 81 | versionhistory 82 | 83 | .. troubleshooting-docs: 84 | .. toctree:: 85 | :maxdepth: 1 86 | :caption: Troubleshooting 87 | 88 | troubleshooting 89 | 90 | .. community-docs: 91 | .. toctree:: 92 | :maxdepth: 1 93 | :caption: Community 94 | 95 | community 96 | 97 | .. apireference-docs: 98 | .. toctree:: 99 | :maxdepth: 1 100 | :caption: API Reference 101 | 102 | py-modindex 103 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/modules/adapters/adapter.rst: -------------------------------------------------------------------------------- 1 | :mod:`ddd.adapters.adapter` 2 | ========================================== 3 | 4 | .. automodule:: ddd.adapters.adapter 5 | 6 | ### 7 | API 8 | ### 9 | 10 | .. autoclass:: Adapter 11 | :members: 12 | -------------------------------------------------------------------------------- /docs/modules/application/action.rst: -------------------------------------------------------------------------------- 1 | :mod:`ddd.application.action` 2 | ========================================== 3 | 4 | The :class:`~ddd.application.application_service.ApplicationService` contains one or more functions that implements the actions. 5 | 6 | .. automodule:: ddd.application.action 7 | 8 | API 9 | --- 10 | 11 | .. autofunction:: action 12 | 13 | 14 | Examples 15 | -------- 16 | 17 | Decorate a function in the application service:: 18 | 19 | @action 20 | async def send_tracking_email(self, command, corr_ids=None): 21 | """ 22 | Send tracking email to recipient. 23 | """ 24 | ... 25 | -------------------------------------------------------------------------------- /docs/modules/application/application_service.rst: -------------------------------------------------------------------------------- 1 | :mod:`ddd.application.application_service` 2 | ========================================== 3 | 4 | .. automodule:: ddd.application.application_service 5 | 6 | API 7 | --- 8 | 9 | .. autoclass:: ApplicationService 10 | :members: 11 | -------------------------------------------------------------------------------- /docs/modules/application/config.rst: -------------------------------------------------------------------------------- 1 | :mod:`ddd.application.config` 2 | ==================================== 3 | 4 | .. automodule:: ddd.application.config 5 | 6 | API 7 | --- 8 | 9 | .. autoclass:: Config 10 | :members: _declare_settings, _read_config 11 | -------------------------------------------------------------------------------- /docs/modules/domain/event/domain_event.rst: -------------------------------------------------------------------------------- 1 | :mod:`ddd.domain.event.domain_event` 2 | ========================================== 3 | 4 | .. automodule:: ddd.domain.event.domain_event 5 | 6 | API 7 | --- 8 | 9 | .. autoclass:: DomainEvent 10 | :members: get_serialized_payload 11 | :show-inheritance: 12 | 13 | 14 | Examples 15 | -------- 16 | 17 | TODO: ... 18 | -------------------------------------------------------------------------------- /docs/modules/domain/event/integration_event.rst: -------------------------------------------------------------------------------- 1 | :mod:`ddd.domain.event.integration_event` 2 | ========================================== 3 | 4 | .. automodule:: ddd.domain.event.integration_event 5 | 6 | API 7 | --- 8 | 9 | .. autoclass:: IntegrationEvent 10 | :members: get_serialized_payload 11 | :show-inheritance: 12 | 13 | 14 | Examples 15 | -------- 16 | 17 | TODO: ... 18 | -------------------------------------------------------------------------------- /docs/modules/infrastructure/db_service/db_service.rst: -------------------------------------------------------------------------------- 1 | :mod:`ddd.infrastructure.db_service` 2 | ==================================== 3 | 4 | The db service is used to acquire connections to the database. 5 | 6 | You will not use this base class directly. Instead, you use one of the implementations: 7 | :class:`~ddd.infrastructure.db_service.postgres_db_service.PostgresDbService` 8 | :class:`~ddd.infrastructure.db_service.memory_db_service.MemoryDbService` or 9 | :class:`~ddd.infrastructure.db_service.memory_postgres_db_service.MemoryPostgresDbService`. 10 | 11 | Look at the documentation to figure out which one to use. 12 | 13 | 14 | .. automodule:: ddd.infrastructure.db_service.db_service 15 | 16 | API 17 | --- 18 | 19 | .. autoclass:: DbService 20 | :members: 21 | -------------------------------------------------------------------------------- /docs/modules/infrastructure/db_service/memory_db_service.rst: -------------------------------------------------------------------------------- 1 | :mod:`ddd.infrastructure.db_service.memory_db_service` 2 | ====================================================== 3 | 4 | .. automodule:: ddd.infrastructure.db_service.memory_db_service 5 | 6 | API 7 | --- 8 | 9 | .. autoclass:: MemoryDbService 10 | :members: 11 | :show-inheritance: 12 | 13 | 14 | Examples 15 | -------- 16 | 17 | Create a ``memory_db_service`` and ``start`` it:: 18 | 19 | from ddd.infrastructure.db_service.memory_db_service import MemoryDbService 20 | 21 | 22 | log_service = ... 23 | 24 | db_service = \ 25 | MemoryDbService( 26 | log_service=log_service, 27 | ) 28 | 29 | await db_service.start() 30 | -------------------------------------------------------------------------------- /docs/modules/infrastructure/db_service/memory_postgres_db_service.rst: -------------------------------------------------------------------------------- 1 | :mod:`ddd.infrastructure.db_service.memory_postgres_db_service` 2 | =============================================================== 3 | 4 | .. automodule:: ddd.infrastructure.db_service.memory_postgres_db_service 5 | 6 | API 7 | --- 8 | 9 | .. autoclass:: MemoryPostgresDbService 10 | :members: 11 | :show-inheritance: 12 | 13 | 14 | Examples 15 | -------- 16 | 17 | Create a ``memory_postgres_db_service`` and ``start`` it:: 18 | 19 | from ddd.infrastructure.db_service.memory_postgres_db_service import MemoryPostgresDbService 20 | 21 | 22 | log_service = ... 23 | 24 | db_service = \ 25 | MemoryPostgresDbService( 26 | log_service=log_service, 27 | min_size=20, 28 | max_size=20, 29 | ) 30 | 31 | await db_service.start() 32 | -------------------------------------------------------------------------------- /docs/modules/infrastructure/db_service/postgres_db_service.rst: -------------------------------------------------------------------------------- 1 | :mod:`ddd.infrastructure.db_service.postgres_db_service` 2 | ======================================================== 3 | 4 | .. automodule:: ddd.infrastructure.db_service.postgres_db_service 5 | 6 | API 7 | --- 8 | 9 | .. autoclass:: PostgresDbService 10 | :members: start, stop 11 | :show-inheritance: 12 | 13 | 14 | Examples 15 | -------- 16 | 17 | Create a ``postgres_db_service`` and ``start`` it:: 18 | 19 | from ddd.infrastructure.db_service.postgres_db_service import PostgresDbService 20 | 21 | 22 | log_service = ... 23 | 24 | db_service = \ 25 | PostgresDbService( 26 | dsn="postgresql://localhost:5432", 27 | log_service=log_service, 28 | min_size=20, 29 | max_size=20, 30 | ) 31 | 32 | await db_service.start() 33 | -------------------------------------------------------------------------------- /docs/modules/utils/dep_mgr/dep_mgr.rst: -------------------------------------------------------------------------------- 1 | :mod:`ddd.utils.dep_mgr` 2 | ==================================== 3 | 4 | .. automodule:: ddd.utils.dep_mgr 5 | 6 | API 7 | --- 8 | 9 | .. autoclass:: DependencyManager 10 | :members: 11 | -------------------------------------------------------------------------------- /docs/modules/utils/tasks/task/task.rst: -------------------------------------------------------------------------------- 1 | :mod:`ddd.utils.tasks.task` 2 | ==================================== 3 | 4 | .. automodule:: ddd.utils.tasks.task 5 | 6 | API 7 | --- 8 | 9 | .. autoclass:: Task 10 | :members: 11 | -------------------------------------------------------------------------------- /docs/py-modindex.rst: -------------------------------------------------------------------------------- 1 | API reference 2 | ============= 3 | 4 | -------------------------------------------------------------------------------- /docs/versionhistory.rst: -------------------------------------------------------------------------------- 1 | ############### 2 | Version history 3 | ############### 4 | 5 | **0.9.5** 6 | 7 | - Added documentation. 8 | - Moved db_service related classes. 9 | - Moved event related classes. 10 | - Added :class:`~ddd.infrastructure.db_service.memory_postgres_db_service.MemoryPostgresDbService` to be able to run tests against an in-memory postgres database. 11 | - Fixed bug: container kwarg in example main.py (thanks euri10). 12 | 13 | .. _documentation: https://ddd-for-python.readthedocs.io/en/latest/ 14 | 15 | **0.9.4** 16 | 17 | - Added ``context`` to log service's log messages. 18 | - Moveed record filtering methods to base repository class. 19 | - Added ``uses_service`` to Task class. Deprecate ``makes_requests``. 20 | 21 | **0.9.3** 22 | 23 | - Searching env file from cwd by default in tests, (when no path specified). 24 | - Refactored Task class to make it more simple. 25 | - Refactored the configuration solution by adding a Config class. 26 | - Added example code for ``shipping`` context of a webshop application. 27 | - Added get_all_jobs() and get_job_count() to scheduler adapter & service. 28 | - Added missing call to _migrate() in a couple of Repository class functions. 29 | 30 | **0.9.2** 31 | 32 | - Fixed bug: Env file wasn't loaded in certain circumstances. 33 | 34 | **0.9.1** 35 | 36 | - Initial commit. 37 | -------------------------------------------------------------------------------- /env.sample: -------------------------------------------------------------------------------- 1 | # General 2 | ENV=development 3 | DEBUG=True 4 | MAX_CONCURRENT_ACTIONS=10 5 | TOP_LEVEL_PACKAGE_NAME= 6 | 7 | # Primary Adapters 8 | HTTP_DEBUG=True 9 | HTTP_PORT=8010 10 | 11 | # Persistence 12 | DATABASE_TYPE=postgres 13 | 14 | POSTGRES_DSN=postgresql://:@:/ 15 | 16 | # Auth 17 | AUTH_FULL_ACCESS_TOKEN= 18 | 19 | # PubSub 20 | DOMAIN_PUBSUB_PROVIDER=memory # memory, kafka or azure 21 | DOMAIN_PUBSUB_TOPIC= 22 | DOMAIN_PUBSUB_GROUP= 23 | 24 | INTERCHANGE_PUBSUB_PROVIDER=memory # memory, kafka or azure 25 | INTERCHANGE_PUBSUB_TOPIC= 26 | INTERCHANGE_PUBSUB_GROUP= 27 | 28 | KAFKA_BOOTSTRAP_SERVERS=localhost:19092 29 | 30 | AZURE_EVENT_HUBS_NAMESPACE=.servicebus.windows.net 31 | AZURE_EVENT_HUBS_NAMESPACE_CONN_STRING= 32 | AZURE_EVENT_HUBS_CHECKPOINT_STORE_CONN_STRING= 33 | AZURE_EVENT_HUBS_BLOB_CONTAINER_NAME=event-hubs-checkpoint-store 34 | 35 | # Jobs 36 | JOBS_SCHEDULER_TYPE=apscheduler 37 | JOBS_SCHEDULER_DSN=postgresql://:@:/ 38 | 39 | # Internal APIs 40 | BASE_URL=http://localhost:8010/ 41 | API_URL=http://localhost:8010//api/v1.0.0 42 | 43 | SLACK_TOKEN= 44 | 45 | # Logging 46 | LOG_KIBANA_ENABLED=false 47 | LOG_KIBANA_URL=https://es.logs.local.app_name.com:443 48 | LOG_KIBANA_USERNAME= 49 | LOG_KIBANA_PASSWORD= 50 | 51 | LOG_SLACK_ENABLED=false 52 | LOG_SLACK_CHANNEL_ERRORS=app-name-errors 53 | 54 | # Tasks 55 | TASK_USERNAME=admin@app_name.com 56 | TASK_PASSWORD= 57 | -------------------------------------------------------------------------------- /env.test: -------------------------------------------------------------------------------- 1 | # General 2 | ENV=development 3 | DEBUG=True 4 | MAX_CONCURRENT_ACTIONS=10 5 | TOP_LEVEL_PACKAGE_NAME=ddd 6 | 7 | # Primary Adapters 8 | HTTP_DEBUG=True 9 | HTTP_PORT=8010 10 | 11 | # Persistence 12 | DATABASE_TYPE=memory 13 | 14 | POSTGRES_DSN=postgresql://test:sYz76*FL834WQ&VxD@127.0.0.1:5434/test 15 | 16 | # Auth 17 | AUTH_FULL_ACCESS_TOKEN= 18 | 19 | # PubSub 20 | DOMAIN_PUBSUB_PROVIDER=memory # memory, kafka or azure 21 | DOMAIN_PUBSUB_TOPIC= 22 | DOMAIN_PUBSUB_GROUP=iam 23 | 24 | INTERCHANGE_PUBSUB_PROVIDER=memory # memory, kafka or azure 25 | INTERCHANGE_PUBSUB_TOPIC= 26 | INTERCHANGE_PUBSUB_GROUP=iam 27 | 28 | KAFKA_BOOTSTRAP_SERVERS=localhost:19092 29 | 30 | AZURE_EVENT_HUBS_NAMESPACE=.servicebus.windows.net 31 | AZURE_EVENT_HUBS_NAMESPACE_CONN_STRING= 32 | AZURE_EVENT_HUBS_CHECKPOINT_STORE_CONN_STRING= 33 | AZURE_EVENT_HUBS_BLOB_CONTAINER_NAME=event-hubs-checkpoint-store 34 | 35 | # Jobs 36 | JOBS_SCHEDULER_TYPE=memory 37 | JOBS_SCHEDULER_DSN=postgresql://:@:/ 38 | 39 | # Internal APIs 40 | BASE_URL=http://localhost:8010/ 41 | APP_NAME_API_URL=http://localhost:8010//api/v1.0.0 42 | 43 | SLACK_TOKEN= 44 | 45 | # Logging 46 | LOG_KIBANA_ENABLED=false 47 | LOG_KIBANA_URL=https://es.logs.local.app_name.com:443 48 | LOG_KIBANA_USERNAME= 49 | LOG_KIBANA_PASSWORD= 50 | 51 | LOG_SLACK_ENABLED=false 52 | LOG_SLACK_CHANNEL_ERRORS=app-name-errors 53 | 54 | # Tasks 55 | TASK_USERNAME=admin@app_name.com 56 | TASK_PASSWORD= 57 | -------------------------------------------------------------------------------- /examples/webshop/README.md: -------------------------------------------------------------------------------- 1 | # Webshop Application 2 | 3 | NOTE: This example is under development. 4 | 5 | ## Description 6 | 7 | This is the webshop sample application. 8 | 9 | It shows how the bounded contexts comprising this application can be implemented using ddd-for-python. 10 | 11 | So far, it contains a single "Shipping" context. More contexts will be added with time, like "Recommendation", "Catalog", "Customer", etc. 12 | -------------------------------------------------------------------------------- /examples/webshop/shipping/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | env 4 | 5 | venv/ 6 | 7 | __pycache__/ 8 | 9 | scratch 10 | -------------------------------------------------------------------------------- /examples/webshop/shipping/Makefile: -------------------------------------------------------------------------------- 1 | ########################################################################## 2 | # This is the project's Makefile. 3 | ########################################################################## 4 | 5 | ########################################################################## 6 | # VARIABLES 7 | ########################################################################## 8 | 9 | HOME := $(shell echo ~) 10 | PWD := $(shell pwd) 11 | ENV := ENV_FILE=env 12 | ENV_TEST := ENV_FILE=env.test 13 | PYTHON := venv/bin/python 14 | 15 | ########################################################################## 16 | # MENU 17 | ########################################################################## 18 | 19 | .PHONY: help 20 | help: 21 | @awk 'BEGIN {FS = ":.*?## "} /^[0-9a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) 22 | 23 | ########################################################################## 24 | # SETUP 25 | ########################################################################## 26 | 27 | .PHONY: create-venv 28 | create-venv: ## create the virtual environment 29 | python3 -m venv $(PWD)/venv 30 | 31 | ########################################################################## 32 | # TEST 33 | ########################################################################## 34 | 35 | .PHONY: test 36 | test: ## run test suite 37 | $(ENV_TEST) $(PYTHON) -m unittest discover src 38 | 39 | ########################################################################## 40 | # COMMANDS 41 | ########################################################################## 42 | 43 | .PHONY: run-task 44 | run-task: ## run a task (uses TASK, ARGS) 45 | @$(ENV) $(PYTHON) src/run_task.py $(TASK) --args "$(ARGS)" 46 | -------------------------------------------------------------------------------- /examples/webshop/shipping/README.md: -------------------------------------------------------------------------------- 1 | # Shipping Context 2 | 3 | NOTE: This example is under development. 4 | 5 | ## Description 6 | 7 | This is the shipping context of the webshop sample application. 8 | -------------------------------------------------------------------------------- /examples/webshop/shipping/env.sample: -------------------------------------------------------------------------------- 1 | # General 2 | ENV=development 3 | DEBUG=True 4 | MAX_CONCURRENT_ACTIONS=10 5 | TOP_LEVEL_PACKAGE_NAME=shipping 6 | 7 | # Loop 8 | LOOP_TYPE=uvloop 9 | 10 | # Primary Adapters 11 | HTTP_DEBUG=True 12 | HTTP_PORT=8010 13 | 14 | # Persistence 15 | DATABASE_TYPE=memory 16 | 17 | POSTGRES_DSN=postgresql://:@:/shipping 18 | 19 | # Auth 20 | AUTH_FULL_ACCESS_TOKEN=fill-in-a-secure-password-here 21 | 22 | # PubSub 23 | DOMAIN_PUBSUB_PROVIDER=memory # memory, kafka or azure 24 | DOMAIN_PUBSUB_TOPIC=webshop.shipping 25 | DOMAIN_PUBSUB_GROUP=shipping 26 | 27 | INTERCHANGE_PUBSUB_PROVIDER=memory # memory, kafka or azure 28 | INTERCHANGE_PUBSUB_TOPIC=webshop.interchange 29 | INTERCHANGE_PUBSUB_GROUP=shipping 30 | 31 | KAFKA_BOOTSTRAP_SERVERS=localhost:19092 32 | 33 | AZURE_EVENT_HUBS_NAMESPACE=shipping.servicebus.windows.net 34 | AZURE_EVENT_HUBS_NAMESPACE_CONN_STRING= 35 | AZURE_EVENT_HUBS_CHECKPOINT_STORE_CONN_STRING= 36 | AZURE_EVENT_HUBS_BLOB_CONTAINER_NAME=event-hubs-checkpoint-store 37 | 38 | # Jobs 39 | JOBS_SCHEDULER_TYPE=memory 40 | JOBS_SCHEDULER_DSN=postgresql://:@:/shipping 41 | 42 | # Slack 43 | SLACK_TOKEN=fill-in-your-slack-token-here 44 | 45 | # Logging 46 | LOG_ENABLED=true 47 | 48 | LOG_KIBANA_ENABLED=false 49 | LOG_KIBANA_URL=https://es.logs.local.shipping.com:443 50 | LOG_KIBANA_USERNAME= 51 | LOG_KIBANA_PASSWORD= 52 | 53 | LOG_SLACK_ENABLED=false 54 | LOG_SLACK_CHANNEL_ERRORS=shipping-errors 55 | 56 | # APIs 57 | BASE_URL=http://localhost:8010/shipping 58 | 59 | # Tasks 60 | TASK_RUNNER_USERNAME=task-runner@example.com 61 | TASK_RUNNER_PASSWORD=fill-in-your-secure-password-here 62 | -------------------------------------------------------------------------------- /examples/webshop/shipping/env.test: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/examples/webshop/shipping/env.test -------------------------------------------------------------------------------- /examples/webshop/shipping/env.test_pipeline: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/examples/webshop/shipping/env.test_pipeline -------------------------------------------------------------------------------- /examples/webshop/shipping/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/examples/webshop/shipping/requirements.txt -------------------------------------------------------------------------------- /examples/webshop/shipping/src/main.py: -------------------------------------------------------------------------------- 1 | from ddd.application.config import Config 2 | from ddd.infrastructure.container import Container 3 | 4 | from shipping.utils.dep_mgr import DependencyManager 5 | from shipping.application.shipping_application_service import \ 6 | ShippingApplicationService 7 | 8 | 9 | if __name__ == "__main__": 10 | """ 11 | This is the container entry point. 12 | Creates the app and runs it in the container. 13 | """ 14 | 15 | # Config 16 | config = Config() 17 | 18 | # Dependency manager 19 | dep_mgr = \ 20 | DependencyManager( 21 | config=config, 22 | ) 23 | 24 | # Application service 25 | service = \ 26 | ShippingApplicationService( 27 | customer_repository=dep_mgr.get_customer_repository(), 28 | db_service=dep_mgr.get_db_service(), 29 | domain_adapter=dep_mgr.get_domain_adapter(), 30 | domain_publisher=dep_mgr.get_domain_publisher(), 31 | event_repository=dep_mgr.get_event_repository(), 32 | interchange_adapter=dep_mgr.get_interchange_adapter(), 33 | interchange_publisher=dep_mgr.get_interchange_publisher(), 34 | job_adapter=dep_mgr.get_job_adapter(), 35 | job_service=dep_mgr.get_job_service(), 36 | log_service=dep_mgr.get_log_service(), 37 | scheduler_adapter=dep_mgr.get_scheduler_adapter(), 38 | shipment_repository=dep_mgr.get_shipment_repository(), 39 | max_concurrent_actions=config.max_concurrent_actions, 40 | loop=config.loop.instance, 41 | ) 42 | 43 | # ..register 44 | dep_mgr.set_service(service) 45 | 46 | # Container 47 | container = \ 48 | Container( 49 | app_service=service, 50 | log_service=dep_mgr.get_log_service(), 51 | ) 52 | 53 | # ..run 54 | loop = config.loop.instance 55 | loop.run_until_complete(container.run()) 56 | loop.close() 57 | -------------------------------------------------------------------------------- /examples/webshop/shipping/src/run_task.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import asyncio 3 | import importlib 4 | import os 5 | 6 | from ddd.utils.utils import load_env_file 7 | 8 | 9 | def _parse_args(): 10 | """ 11 | Parse the CLI arguments. 12 | """ 13 | parser = argparse.ArgumentParser() 14 | 15 | parser.add_argument("task", help="the filename of the task", type=str) 16 | parser.add_argument("--args", help="the task arguments", type=str) 17 | 18 | args = parser.parse_args() 19 | 20 | return args.task, args.args 21 | 22 | def read_config(): 23 | """ 24 | Read config dict using 'read_config()' in utils. 25 | 26 | NOTE: 27 | 'TOP_LEVEL_PACKAGE_NAME' needs to be defined in the env file. 28 | """ 29 | path = f"{os.getenv('TOP_LEVEL_PACKAGE_NAME')}.utils.utils" 30 | 31 | module = importlib.import_module(path, package=None) 32 | 33 | func = getattr(module, "read_config") 34 | 35 | config = func() 36 | 37 | return config 38 | 39 | def find_task_class(config, task_name): 40 | 41 | # Search in project's 'tasks' folder 42 | try: 43 | path = f"{config.top_level_package_name}.utils.tasks.{task_name}" 44 | module = importlib.import_module(path, package=None) 45 | task_class = getattr(module, "Task") 46 | except ModuleNotFoundError: 47 | 48 | # Search in ddd lib's 'tasks' folder 49 | module = importlib.import_module(f"ddd.utils.tasks.{task_name}", package=None) 50 | task_class = getattr(module, "Task") 51 | 52 | return task_class 53 | 54 | 55 | if __name__ == "__main__": 56 | """ 57 | Run the task. 58 | """ 59 | # Config 60 | load_env_file() 61 | config = read_config() 62 | 63 | # Get dependency manager 64 | path = f"{config.top_level_package_name}.utils.dep_mgr" 65 | 66 | module = importlib.import_module(path, package=None) 67 | 68 | mgr_class = getattr(module, "DependencyManager") 69 | 70 | deps_mgr = \ 71 | mgr_class( 72 | config=config, 73 | ) 74 | 75 | # Args 76 | task, args = _parse_args() 77 | 78 | # Get task 79 | task_class = find_task_class(config=config, task_name=task) 80 | 81 | # Run 82 | task = \ 83 | task_class( 84 | config=config, 85 | deps_mgr=deps_mgr, 86 | args_str=args, 87 | ) 88 | 89 | asyncio.get_event_loop().run_until_complete( 90 | task._run() 91 | ) 92 | -------------------------------------------------------------------------------- /examples/webshop/shipping/src/shipping/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/examples/webshop/shipping/src/shipping/__init__.py -------------------------------------------------------------------------------- /examples/webshop/shipping/src/shipping/adapters/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/examples/webshop/shipping/src/shipping/adapters/__init__.py -------------------------------------------------------------------------------- /examples/webshop/shipping/src/shipping/adapters/http/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/examples/webshop/shipping/src/shipping/adapters/http/__init__.py -------------------------------------------------------------------------------- /examples/webshop/shipping/src/shipping/adapters/listeners/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/examples/webshop/shipping/src/shipping/adapters/listeners/__init__.py -------------------------------------------------------------------------------- /examples/webshop/shipping/src/shipping/adapters/listeners/domain/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/examples/webshop/shipping/src/shipping/adapters/listeners/domain/__init__.py -------------------------------------------------------------------------------- /examples/webshop/shipping/src/shipping/adapters/listeners/domain/shipment_created_listener.py: -------------------------------------------------------------------------------- 1 | from ddd.adapters.event.event_listener import EventListener 2 | 3 | from shipping.domain.shipment.shipment_id import ShipmentId 4 | from shipping.domain.shipment.shipment_created import ShipmentCreated 5 | 6 | 7 | class ShipmentCreatedListener(EventListener): 8 | def __init__( 9 | self, 10 | action, 11 | service, 12 | command_creator 13 | ): 14 | super().__init__( 15 | "ShipmentCreated", 16 | action=action, 17 | service=service, 18 | command_creator=command_creator 19 | ) 20 | 21 | def read_event(self): 22 | super().read_event() 23 | 24 | # Customer ID 25 | shipment_id = \ 26 | self.reader.entity_id_value( 27 | 'payload.shipment_id', ShipmentId 28 | ) 29 | 30 | # Event 31 | return ShipmentCreated( 32 | shipment_id=shipment_id, 33 | corr_ids=self.corr_ids, 34 | ) 35 | -------------------------------------------------------------------------------- /examples/webshop/shipping/src/shipping/application/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/examples/webshop/shipping/src/shipping/application/__init__.py -------------------------------------------------------------------------------- /examples/webshop/shipping/src/shipping/application/config.py: -------------------------------------------------------------------------------- 1 | from ddd.application.config import Config as BaseConfig 2 | 3 | 4 | class Config(BaseConfig): 5 | def __init__(self): 6 | super().__init__() 7 | -------------------------------------------------------------------------------- /examples/webshop/shipping/src/shipping/application/shipping_application_service.py: -------------------------------------------------------------------------------- 1 | from ddd.application.application_service import ApplicationService 2 | from ddd.application.action import action 3 | 4 | 5 | class ShippingApplicationService(ApplicationService): 6 | """ 7 | This is the context's application service. 8 | """ 9 | def __init__( 10 | self, 11 | customer_repository, 12 | db_service, 13 | domain_adapter, 14 | domain_publisher, 15 | event_repository, 16 | interchange_adapter, 17 | interchange_publisher, 18 | job_adapter, 19 | job_service, 20 | log_service, 21 | scheduler_adapter, 22 | shipment_repository, 23 | max_concurrent_actions, 24 | loop, 25 | ): 26 | """ 27 | Initialize the application service. 28 | """ 29 | super().__init__( 30 | db_service=db_service, 31 | domain_adapter=domain_adapter, 32 | domain_publisher=domain_publisher, 33 | event_repository=event_repository, 34 | interchange_adapter=interchange_adapter, 35 | interchange_publisher=interchange_publisher, 36 | job_adapter=job_adapter, 37 | job_service=job_service, 38 | log_service=log_service, 39 | scheduler_adapter=scheduler_adapter, 40 | max_concurrent_actions=max_concurrent_actions, 41 | loop=loop, 42 | ) 43 | 44 | # References 45 | self.customer_repository = customer_repository 46 | self.shipment_repository = shipment_repository 47 | 48 | # Classify deps 49 | self.domain_services.extend([ 50 | 51 | ]) 52 | 53 | self.primary_adapters.extend([ 54 | 55 | ]) 56 | 57 | self.secondary_adapters.extend([ 58 | customer_repository, 59 | shipment_repository, 60 | ]) 61 | 62 | # Actions 63 | 64 | @action 65 | async def send_tracking_email(self, command, corr_ids=None): 66 | """ 67 | Send tracking email to recipient. 68 | """ 69 | corr_ids = corr_ids if corr_ids is not None else [] 70 | 71 | raise NotImplementedError() 72 | -------------------------------------------------------------------------------- /examples/webshop/shipping/src/shipping/domain/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/examples/webshop/shipping/src/shipping/domain/__init__.py -------------------------------------------------------------------------------- /examples/webshop/shipping/src/shipping/domain/commands.py: -------------------------------------------------------------------------------- 1 | """ 2 | The domain commands. 3 | """ 4 | from ddd.domain.command import Command 5 | 6 | 7 | # Actions 8 | 9 | class SendTrackingEmailCommand(Command): 10 | def __init__( 11 | self, 12 | shipment_id, 13 | token, 14 | ): 15 | super().__init__() 16 | self.shipment_id = shipment_id 17 | self.token = token 18 | -------------------------------------------------------------------------------- /examples/webshop/shipping/src/shipping/domain/customer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/examples/webshop/shipping/src/shipping/domain/customer/__init__.py -------------------------------------------------------------------------------- /examples/webshop/shipping/src/shipping/domain/customer/customer.py: -------------------------------------------------------------------------------- 1 | from ddd.domain.aggregate import Aggregate 2 | 3 | 4 | class Customer(Aggregate): 5 | """ 6 | A customer. 7 | """ 8 | VERSION = "1" 9 | 10 | def __init__( 11 | self, 12 | version, 13 | customer_id, 14 | created_at, 15 | updated_at, 16 | email, 17 | first_name, 18 | last_name, 19 | ): 20 | super().__init__(version=version) 21 | self.customer_id = customer_id 22 | self.created_at = created_at 23 | self.updated_at = updated_at 24 | self.email = email 25 | self.first_name = first_name 26 | self.last_name = last_name 27 | -------------------------------------------------------------------------------- /examples/webshop/shipping/src/shipping/domain/customer/customer_id.py: -------------------------------------------------------------------------------- 1 | from ddd.domain.entity_id import EntityId 2 | 3 | 4 | class CustomerId(EntityId): 5 | """ 6 | A customer ID. 7 | """ 8 | def __init__(self, identity): 9 | super().__init__(identity=identity) 10 | -------------------------------------------------------------------------------- /examples/webshop/shipping/src/shipping/domain/customer/customer_repository.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | 3 | 4 | class CustomerRepository(object, metaclass=ABCMeta): 5 | def __init__( 6 | self, 7 | ): 8 | CustomerRepository.__init__(self) 9 | 10 | # Operations 11 | 12 | @abstractmethod 13 | async def get(self, customer_id): 14 | pass 15 | 16 | @abstractmethod 17 | async def save(self, customer): 18 | pass 19 | 20 | @abstractmethod 21 | async def delete(self, customer): 22 | pass 23 | 24 | @abstractmethod 25 | async def get_with_id( 26 | self, 27 | customer_id, 28 | ): 29 | pass 30 | 31 | @abstractmethod 32 | async def get_with_ids( 33 | self, 34 | customer_ids, 35 | ): 36 | pass 37 | -------------------------------------------------------------------------------- /examples/webshop/shipping/src/shipping/domain/customer/customer_translator.py: -------------------------------------------------------------------------------- 1 | from ddd.adapters.message_reader import MessageReader 2 | 3 | from ddd.utils.utils import str_or_none 4 | from ddd.utils.utils import iso_or_none 5 | 6 | 7 | class CustomerTranslator(object): 8 | def __init__(self, customer): 9 | super().__init__() 10 | self.customer = customer 11 | 12 | def to_domain(self): 13 | return self.to_dict() 14 | 15 | @classmethod 16 | def from_domain(cls, the_dict): 17 | from shipping.domain.customer.customer import Customer 18 | from shipping.domain.customer.customer_id import CustomerId 19 | 20 | reader = MessageReader(the_dict) 21 | 22 | # Read 23 | customer = Customer( 24 | version=reader.string_value('version'), 25 | customer_id=reader.entity_id_value('customer_id', CustomerId), 26 | created_at=reader.date_value('created_at'), 27 | updated_at=reader.date_value('updated_at'), 28 | email=reader.string_value('email'), 29 | first_name=reader.string_value('first_name'), 30 | last_name=reader.string_value('last_name'), 31 | ) 32 | 33 | return customer 34 | 35 | def to_record(self): 36 | return self.to_dict() 37 | 38 | @classmethod 39 | def from_record(cls, record): 40 | the_dict = record['data'] 41 | the_dict['customer_id'] = record['id'] 42 | 43 | return CustomerTranslator.from_domain(the_dict) 44 | 45 | def to_dict(self): 46 | return { 47 | 'version': self.customer.version, 48 | 'user_id': str_or_none(self.customer.customer_id), 49 | 'created_at': iso_or_none(self.customer.created_at), 50 | 'updated_at': iso_or_none(self.customer.updated_at), 51 | 'email': self.customer.email, 52 | 'first_name': self.customer.first_name, 53 | 'last_name': self.customer.last_name, 54 | } 55 | -------------------------------------------------------------------------------- /examples/webshop/shipping/src/shipping/domain/shipment/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/examples/webshop/shipping/src/shipping/domain/shipment/__init__.py -------------------------------------------------------------------------------- /examples/webshop/shipping/src/shipping/domain/shipment/shipment.py: -------------------------------------------------------------------------------- 1 | from ddd.domain.aggregate import Aggregate 2 | 3 | 4 | class Shipment(Aggregate): 5 | """ 6 | A shipment. 7 | """ 8 | VERSION = "1" 9 | 10 | def __init__( 11 | self, 12 | version, 13 | shipment_id, 14 | created_at, 15 | updated_at, 16 | customer_id, 17 | ): 18 | super().__init__(version=version) 19 | self.shipment_id = shipment_id 20 | self.created_at = created_at 21 | self.updated_at = updated_at 22 | self.customer_id = customer_id 23 | -------------------------------------------------------------------------------- /examples/webshop/shipping/src/shipping/domain/shipment/shipment_created.py: -------------------------------------------------------------------------------- 1 | from ddd.domain.domain_event import DomainEvent 2 | 3 | 4 | class ShipmentCreated(DomainEvent): 5 | def __init__(self, shipment_id, corr_ids): 6 | super().__init__( 7 | name="ShipmentCreated", 8 | corr_ids=corr_ids, 9 | ) 10 | self.shipment_id = shipment_id 11 | 12 | def get_serialized_payload(self): 13 | return { 14 | 'shipment_id': str(self.shipment_id), 15 | } 16 | -------------------------------------------------------------------------------- /examples/webshop/shipping/src/shipping/domain/shipment/shipment_id.py: -------------------------------------------------------------------------------- 1 | from ddd.domain.entity_id import EntityId 2 | 3 | 4 | class ShipmentId(EntityId): 5 | """ 6 | A shipment ID. 7 | """ 8 | def __init__(self, identity): 9 | super().__init__(identity=identity) 10 | -------------------------------------------------------------------------------- /examples/webshop/shipping/src/shipping/domain/shipment/shipment_repository.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | 3 | 4 | class ShipmentRepository(object, metaclass=ABCMeta): 5 | def __init__( 6 | self, 7 | ): 8 | ShipmentRepository.__init__(self) 9 | 10 | # Operations 11 | 12 | @abstractmethod 13 | async def get(self, shipment_id): 14 | pass 15 | 16 | @abstractmethod 17 | async def save(self, shipment): 18 | pass 19 | 20 | @abstractmethod 21 | async def delete(self, shipment): 22 | pass 23 | 24 | @abstractmethod 25 | async def get_with_id( 26 | self, 27 | shipment_id, 28 | ): 29 | pass 30 | 31 | @abstractmethod 32 | async def get_with_ids( 33 | self, 34 | shipment_ids, 35 | ): 36 | pass 37 | -------------------------------------------------------------------------------- /examples/webshop/shipping/src/shipping/domain/shipment/shipment_translator.py: -------------------------------------------------------------------------------- 1 | from ddd.adapters.message_reader import MessageReader 2 | 3 | from ddd.utils.utils import str_or_none 4 | from ddd.utils.utils import iso_or_none 5 | 6 | 7 | class ShipmentTranslator(object): 8 | def __init__(self, shipment): 9 | super().__init__() 10 | self.shipment = shipment 11 | 12 | def to_domain(self): 13 | return self.to_dict() 14 | 15 | @classmethod 16 | def from_domain(cls, the_dict): 17 | from shipping.domain.shipment.shipment import Shipment 18 | from shipping.domain.shipment.shipment_id import ShipmentId 19 | from shipping.domain.customer.customer_id import CustomerId 20 | 21 | reader = MessageReader(the_dict) 22 | 23 | # Read 24 | shipment = Shipment( 25 | version=reader.string_value('version'), 26 | shipment_id=reader.entity_id_value('shipment_id', ShipmentId), 27 | created_at=reader.date_value('created_at'), 28 | updated_at=reader.date_value('updated_at'), 29 | customer_id=reader.entity_id_value('customer_id', CustomerId), 30 | ) 31 | 32 | return shipment 33 | 34 | def to_record(self): 35 | return self.to_dict() 36 | 37 | @classmethod 38 | def from_record(cls, record): 39 | the_dict = record['data'] 40 | the_dict['shipment_id'] = record['id'] 41 | 42 | return ShipmentTranslator.from_domain(the_dict) 43 | 44 | def to_dict(self): 45 | return { 46 | 'version': self.shipment.version, 47 | 'shipment_id': str_or_none(self.shipment.shipment_id), 48 | 'created_at': iso_or_none(self.shipment.created_at), 49 | 'updated_at': iso_or_none(self.shipment.updated_at), 50 | 'customer_id': str_or_none(self.shipment.customer_id), 51 | } 52 | -------------------------------------------------------------------------------- /examples/webshop/shipping/src/shipping/repositories/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/examples/webshop/shipping/src/shipping/repositories/__init__.py -------------------------------------------------------------------------------- /examples/webshop/shipping/src/shipping/repositories/memory/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/examples/webshop/shipping/src/shipping/repositories/memory/__init__.py -------------------------------------------------------------------------------- /examples/webshop/shipping/src/shipping/repositories/memory/memory_customer_repository.py: -------------------------------------------------------------------------------- 1 | from ddd.repositories.memory.memory_repository import MemoryRepository 2 | 3 | from shipping.domain.customer.customer import Customer 4 | from shipping.domain.customer.customer_repository import CustomerRepository 5 | from shipping.domain.customer.customer_translator import CustomerTranslator 6 | 7 | 8 | class MemoryCustomerRepository(CustomerRepository, MemoryRepository): 9 | 10 | def __init__(self, log_service): 11 | MemoryRepository.__init__( 12 | self, 13 | log_service=log_service, 14 | aggregate_name="customer", 15 | aggregate_cls=Customer, 16 | translator_cls=CustomerTranslator, 17 | ) 18 | 19 | async def get(self, customer_id): 20 | return await super()._get(customer_id) 21 | 22 | async def save(self, customer): 23 | await super()._save( 24 | aggregate_id=customer.customer_id, 25 | aggregate=customer, 26 | ) 27 | 28 | async def delete(self, customer): 29 | await self._delete(customer.customer_id) 30 | 31 | async def get_with_id( 32 | self, 33 | customer_id, 34 | ): 35 | customers = await self.get_with_ids(customer_ids=[customer_id]) 36 | 37 | if len(customers): 38 | return customers[0] 39 | 40 | return None 41 | 42 | async def get_with_ids( 43 | self, 44 | customer_ids, 45 | ): 46 | records = [ 47 | r for r in self.records.values() 48 | if r['data']['customer_id'] in customer_ids 49 | ] 50 | 51 | records = self._migrate(records, self.aggregate_cls) 52 | 53 | aggregates = [ 54 | self.aggregate_from_record(record) 55 | for record in records 56 | ] 57 | 58 | return aggregates 59 | -------------------------------------------------------------------------------- /examples/webshop/shipping/src/shipping/repositories/memory/memory_shipment_repository.py: -------------------------------------------------------------------------------- 1 | from ddd.repositories.memory.memory_repository import MemoryRepository 2 | 3 | from shipping.domain.shipment.shipment import Shipment 4 | from shipping.domain.shipment.shipment_repository import ShipmentRepository 5 | from shipping.domain.shipment.shipment_translator import ShipmentTranslator 6 | 7 | 8 | class MemoryShipmentRepository(ShipmentRepository, MemoryRepository): 9 | 10 | def __init__(self, log_service): 11 | MemoryRepository.__init__( 12 | self, 13 | log_service=log_service, 14 | aggregate_name="shipment", 15 | aggregate_cls=Shipment, 16 | translator_cls=ShipmentTranslator, 17 | ) 18 | 19 | async def get(self, shipment_id): 20 | return await super()._get(shipment_id) 21 | 22 | async def save(self, shipment): 23 | await super()._save( 24 | aggregate_id=shipment.shipment_id, 25 | aggregate=shipment, 26 | ) 27 | 28 | async def delete(self, shipment): 29 | await self._delete(shipment.shipment_id) 30 | 31 | async def get_with_id( 32 | self, 33 | shipment_id, 34 | ): 35 | shipments = await self.get_with_ids(shipment_ids=[shipment_id]) 36 | 37 | if len(shipments): 38 | return shipments[0] 39 | 40 | return None 41 | 42 | async def get_with_ids( 43 | self, 44 | shipment_ids, 45 | ): 46 | records = [ 47 | r for r in self.records.values() 48 | if r['data']['shipment_id'] in shipment_ids 49 | ] 50 | 51 | records = self._migrate(records, self.aggregate_cls) 52 | 53 | aggregates = [ 54 | self.aggregate_from_record(record) 55 | for record in records 56 | ] 57 | 58 | return aggregates 59 | -------------------------------------------------------------------------------- /examples/webshop/shipping/src/shipping/repositories/postgres/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/examples/webshop/shipping/src/shipping/repositories/postgres/__init__.py -------------------------------------------------------------------------------- /examples/webshop/shipping/src/shipping/repositories/postgres/postgres_customer_repository.py: -------------------------------------------------------------------------------- 1 | from ddd.repositories.postgres.postgres_repository import PostgresRepository 2 | 3 | from shipping.domain.customer.customer import Customer 4 | from shipping.domain.customer.customer_repository import CustomerRepository 5 | from shipping.domain.customer.customer_translator import CustomerTranslator 6 | 7 | 8 | class PostgresCustomerRepository(CustomerRepository, PostgresRepository): 9 | def __init__( 10 | self, 11 | config, 12 | db_service, 13 | log_service, 14 | loop=None, 15 | ): 16 | PostgresRepository.__init__( 17 | self, 18 | loop=loop, 19 | db_service=db_service, 20 | log_service=log_service, 21 | aggregate_cls=Customer, 22 | translator_cls=CustomerTranslator, 23 | table_name="webshop_customers", 24 | dsn=config.database.postgres.dsn, 25 | ) 26 | 27 | # Operations 28 | 29 | async def get(self, customer_id): 30 | return await self._get(aggregate_id=customer_id) 31 | 32 | async def save(self, customer): 33 | await self._save( 34 | aggregate_id=customer.customer_id, 35 | aggregate=customer, 36 | ) 37 | 38 | async def delete(self, customer): 39 | await self._delete(aggregate_id=customer.customer_id) 40 | 41 | async def get_with_id( 42 | self, 43 | customer_id, 44 | ): 45 | customers = await self.get_with_ids(customer_ids=[customer_id]) 46 | 47 | if len(customers): 48 | return customers[0] 49 | 50 | return None 51 | 52 | async def get_with_ids( 53 | self, 54 | customer_ids, 55 | ): 56 | async with self.db_service.conn_pool.acquire() as conn: 57 | records = await conn.fetch( 58 | """SELECT * FROM {}""". 59 | format( 60 | self.table_name 61 | ) 62 | ) 63 | 64 | records = self._migrate(records, Customer) 65 | 66 | records = \ 67 | self._filter_by_property( 68 | records=records, 69 | property="customer_id", 70 | values=customer_ids, 71 | ) 72 | 73 | aggregates = [ 74 | self.aggregate_from_record(record) 75 | for record in records 76 | ] 77 | 78 | aggregates.sort(key=lambda r: r.created_at, reverse=True) 79 | 80 | return aggregates 81 | -------------------------------------------------------------------------------- /examples/webshop/shipping/src/shipping/repositories/postgres/postgres_shipment_repository.py: -------------------------------------------------------------------------------- 1 | from ddd.repositories.postgres.postgres_repository import PostgresRepository 2 | 3 | from shipping.domain.shipment.shipment import Shipment 4 | from shipping.domain.shipment.shipment_repository import ShipmentRepository 5 | from shipping.domain.shipment.shipment_translator import ShipmentTranslator 6 | 7 | 8 | class PostgresShipmentRepository(ShipmentRepository, PostgresRepository): 9 | def __init__( 10 | self, 11 | config, 12 | db_service, 13 | log_service, 14 | loop=None, 15 | ): 16 | PostgresRepository.__init__( 17 | self, 18 | loop=loop, 19 | db_service=db_service, 20 | log_service=log_service, 21 | aggregate_cls=Shipment, 22 | translator_cls=ShipmentTranslator, 23 | table_name="shipping_shipments", 24 | dsn=config.database.postgres.dsn, 25 | ) 26 | 27 | # Operations 28 | 29 | async def get(self, shipment_id): 30 | return await self._get(aggregate_id=shipment_id) 31 | 32 | async def save(self, shipment): 33 | await self._save( 34 | aggregate_id=shipment.shipment_id, 35 | aggregate=shipment, 36 | ) 37 | 38 | async def delete(self, shipment): 39 | await self._delete(aggregate_id=shipment.shipment_id) 40 | 41 | async def get_with_id( 42 | self, 43 | shipment_id, 44 | ): 45 | shipments = await self.get_with_ids(shipment_ids=[shipment_id]) 46 | 47 | if len(shipments): 48 | return shipments[0] 49 | 50 | return None 51 | 52 | async def get_with_ids( 53 | self, 54 | shipment_ids, 55 | ): 56 | async with self.db_service.conn_pool.acquire() as conn: 57 | records = await conn.fetch( 58 | """SELECT * FROM {}""". 59 | format( 60 | self.table_name 61 | ) 62 | ) 63 | 64 | records = self._migrate(records, Shipment) 65 | 66 | records = \ 67 | self._filter_by_property( 68 | records=records, 69 | property="shipment_id", 70 | values=shipment_ids, 71 | ) 72 | 73 | aggregates = [ 74 | self.aggregate_from_record(record) 75 | for record in records 76 | ] 77 | 78 | aggregates.sort(key=lambda r: r.created_at, reverse=True) 79 | 80 | return aggregates 81 | -------------------------------------------------------------------------------- /examples/webshop/shipping/src/shipping/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/examples/webshop/shipping/src/shipping/tests/__init__.py -------------------------------------------------------------------------------- /examples/webshop/shipping/src/shipping/tests/domain/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/examples/webshop/shipping/src/shipping/tests/domain/__init__.py -------------------------------------------------------------------------------- /examples/webshop/shipping/src/shipping/tests/domain/actions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/examples/webshop/shipping/src/shipping/tests/domain/actions/__init__.py -------------------------------------------------------------------------------- /examples/webshop/shipping/src/shipping/tests/shipping_action_test_case.py: -------------------------------------------------------------------------------- 1 | from ddd.tests.action_test_case import ActionTestCase 2 | 3 | from shipping.application.shipping_application_service import \ 4 | ShippingApplicationService 5 | from shipping.utils.dep_mgr import DependencyManager 6 | 7 | 8 | class ShippingActionTestCase(ActionTestCase): 9 | 10 | def __init__(self, methodName='runTest'): 11 | super().__init__(methodName=methodName) 12 | 13 | async def asyncSetUp(self): 14 | await super().asyncSetUp() 15 | 16 | async def asyncTearDown(self): 17 | await self.deps.get_customer_repository().delete_all() 18 | 19 | await super().asyncTearDown() 20 | 21 | # Dependencies 22 | 23 | def read_config(self): 24 | self.config = read_config() 25 | 26 | def _create_deps_manager(self): 27 | self.deps = DependencyManager( 28 | config=self.config, 29 | loop=self.loop, 30 | ) 31 | 32 | def get_service(self): 33 | if not self.service: 34 | self.service = \ 35 | ShippingApplicationService( 36 | customer_repository= 37 | self.deps.get_customer_repository(), 38 | db_service=self.deps.get_db_service(), 39 | domain_adapter=self.deps.get_domain_adapter(), 40 | domain_publisher=self.deps.get_domain_publisher(), 41 | event_repository=self.deps.get_event_repository(), 42 | interchange_adapter=self.deps.get_interchange_adapter(), 43 | interchange_publisher= 44 | self.deps.get_interchange_publisher(), 45 | job_adapter=self.deps.get_job_adapter(), 46 | job_service=self.deps.get_job_service(), 47 | log_service=self.deps.get_log_service(), 48 | scheduler_adapter=self.deps.get_scheduler_adapter(), 49 | shipment_repository= 50 | self.deps.get_shipment_repository(), 51 | max_concurrent_actions=self.config.max_concurrent_actions, 52 | ) 53 | 54 | return self.service 55 | 56 | # Actions 57 | 58 | 59 | 60 | # Helpers 61 | 62 | def _disable_event_listeners(self): 63 | self._disable_domain_event_listeners() 64 | self._disable_integration_event_listeners() 65 | 66 | def _disable_domain_event_listeners(self, exclude=None): 67 | exclude = exclude if exclude is not None else [] 68 | 69 | exclude = [ 70 | e.__name__ for e in exclude 71 | ] 72 | 73 | for listener in self.deps.get_domain_adapter().listeners: 74 | if listener.listens_to_name not in exclude: 75 | listener.disable() 76 | else: 77 | listener.enable() 78 | 79 | def _disable_integration_event_listeners(self, exclude=None): 80 | exclude = exclude if exclude is not None else [] 81 | 82 | exclude = [ 83 | e.__name__ for e in exclude 84 | ] 85 | 86 | for listener in self.deps.get_interchange_adapter().listeners: 87 | if listener.listens_to_name not in exclude: 88 | listener.disable() 89 | else: 90 | listener.enable() 91 | -------------------------------------------------------------------------------- /examples/webshop/shipping/src/shipping/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/examples/webshop/shipping/src/shipping/utils/__init__.py -------------------------------------------------------------------------------- /examples/webshop/shipping/src/shipping/utils/tasks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/examples/webshop/shipping/src/shipping/utils/tasks/__init__.py -------------------------------------------------------------------------------- /examples/webshop/shipping/src/shipping/utils/tasks/setup_dev_data.py: -------------------------------------------------------------------------------- 1 | from ddd.utils.tasks.task import Task as BaseTask 2 | 3 | 4 | class Task(BaseTask): 5 | 6 | def __init__(self, config, deps_mgr, args_str): 7 | super().__init__( 8 | config=config, 9 | deps_mgr=deps_mgr, 10 | args_str=args_str, 11 | makes_requests=False, 12 | ) 13 | 14 | def add_args(self, parser): 15 | parser.add_argument( 16 | "--dummy", 17 | help="a dummy argument, ignored", 18 | type=str, 19 | ) 20 | 21 | async def run(self): 22 | raise NotImplementedError() 23 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | addict==2.3.0 2 | aiokafka==0.7.0 3 | aiosonic==0.7.2 4 | APScheduler==3.6.3 5 | arrow==0.16.0 6 | asyncpg==0.21.0 7 | azure-eventhub==5.2.0 8 | azure-eventhub-checkpointstoreblob-aio==1.1.1 9 | CMRESHandler==1.0.0 10 | docutils==0.16 11 | python-dotenv==0.14.0 12 | sanic==20.9.1 13 | slacker-log-handler==1.7.1 14 | Sphinx==4.4.0 15 | sphinx-autobuild==2021.3.14 16 | sphinx-rtd-theme==1.0.0 17 | SQLAlchemy==1.3.20 18 | testing.postgresql==1.3.0 19 | twine==3.3.0 20 | uvloop==0.14.0 21 | wheel==0.34.2 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages 2 | from setuptools import setup 3 | 4 | with open("README.md", "r", encoding="utf-8") as fh: 5 | long_description = fh.read() 6 | 7 | setup( 8 | name='ddd-for-python', 9 | version='0.9.5', 10 | author='David Runemalm, 2021', 11 | author_email='david.runemalm@gmail.com', 12 | description= 13 | 'A domain-driven design (DDD) framework for Python.', 14 | long_description=long_description, 15 | long_description_content_type="text/markdown", 16 | url='https://github.com/runemalm/ddd-for-python', 17 | project_urls={ 18 | "Documentation": "https://ddd-for-python.readthedocs.io/en/latest/", 19 | "Bug Tracker": "https://github.com/runemalm/ddd-for-python/issues", 20 | }, 21 | package_dir={'': '.'}, 22 | packages=find_packages( 23 | where='.', 24 | include=['ddd*',], 25 | exclude=[] 26 | ), 27 | license='GNU General Public License v3.0', 28 | install_requires=[ 29 | 'addict>=2.3.0', 30 | 'aiokafka>=0.7.0', 31 | 'APScheduler>=3.6.3', 32 | 'arrow>=0.16.0', 33 | 'asyncpg>=0.21.0', 34 | 'azure-eventhub>=5.2.0', 35 | 'azure-eventhub-checkpointstoreblob-aio>=1.1.1', 36 | 'CMRESHandler>=1.0.0', 37 | 'python-dotenv>=0.14.0', 38 | 'slacker-log-handler>=1.7.1', 39 | 'SQLAlchemy>=1.3.20', 40 | 'uvloop>=0.14.0', 41 | ], 42 | tests_require=[ 43 | 'aiosonic>=0.7.2', 44 | ], 45 | python_requires='>=3.8.5', 46 | ) 47 | -------------------------------------------------------------------------------- /templates/project/README.md: -------------------------------------------------------------------------------- 1 | # Webshop Application 2 | 3 | NOTE: This example is under development. 4 | 5 | ## Description 6 | 7 | This is the webshop sample application. 8 | 9 | It shows how the bounded contexts comprising this application can be implemented using ddd-for-python. 10 | 11 | So far, it contains a single "Shipping" context. More contexts will be added with time, like "Recommendation", "Catalog", "Customer", etc. 12 | -------------------------------------------------------------------------------- /templates/project/domain/Makefile: -------------------------------------------------------------------------------- 1 | ########################################################################## 2 | # This is the project's Makefile. 3 | ########################################################################## 4 | 5 | ########################################################################## 6 | # VARIABLES 7 | ########################################################################## 8 | 9 | HOME := $(shell echo ~) 10 | PWD := $(shell pwd) 11 | ENV := ENV_FILE=env 12 | ENV_TEST := ENV_FILE=env.test 13 | PYTHON := venv/bin/python 14 | 15 | ########################################################################## 16 | # MENU 17 | ########################################################################## 18 | 19 | .PHONY: help 20 | help: 21 | @awk 'BEGIN {FS = ":.*?## "} /^[0-9a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) 22 | 23 | ########################################################################## 24 | # SETUP 25 | ########################################################################## 26 | 27 | .PHONY: create-venv 28 | create-venv: ## create the virtual environment 29 | python3 -m venv $(PWD)/venv 30 | 31 | ########################################################################## 32 | # TEST 33 | ########################################################################## 34 | 35 | .PHONY: test 36 | test: ## run test suite 37 | $(ENV_TEST) $(PYTHON) -m unittest discover src 38 | 39 | ########################################################################## 40 | # COMMANDS 41 | ########################################################################## 42 | 43 | .PHONY: run-task 44 | run-task: ## run a task (uses TASK, ARGS) 45 | @$(ENV) $(PYTHON) src/run_task.py $(TASK) --args "$(ARGS)" 46 | -------------------------------------------------------------------------------- /templates/project/domain/README.md: -------------------------------------------------------------------------------- 1 | # Shipping Context 2 | 3 | NOTE: This example is under development. 4 | 5 | ## Description 6 | 7 | This is the shipping context of the webshop sample application. 8 | -------------------------------------------------------------------------------- /templates/project/domain/env.sample: -------------------------------------------------------------------------------- 1 | # General 2 | ENV=development 3 | DEBUG=True 4 | MAX_CONCURRENT_ACTIONS=10 5 | TOP_LEVEL_PACKAGE_NAME=shipping 6 | 7 | # Loop 8 | LOOP_TYPE=uvloop 9 | 10 | # Primary Adapters 11 | HTTP_DEBUG=True 12 | HTTP_PORT=8010 13 | 14 | # Persistence 15 | DATABASE_TYPE=memory 16 | 17 | POSTGRES_DSN=postgresql://:@:/shipping 18 | 19 | # Auth 20 | AUTH_FULL_ACCESS_TOKEN=fill-in-a-secure-password-here 21 | 22 | # PubSub 23 | DOMAIN_PUBSUB_PROVIDER=memory # memory, kafka or azure 24 | DOMAIN_PUBSUB_TOPIC=webshop.shipping 25 | DOMAIN_PUBSUB_GROUP=shipping 26 | 27 | INTERCHANGE_PUBSUB_PROVIDER=memory # memory, kafka or azure 28 | INTERCHANGE_PUBSUB_TOPIC=webshop.interchange 29 | INTERCHANGE_PUBSUB_GROUP=shipping 30 | 31 | KAFKA_BOOTSTRAP_SERVERS=localhost:19092 32 | 33 | AZURE_EVENT_HUBS_NAMESPACE=shipping.servicebus.windows.net 34 | AZURE_EVENT_HUBS_NAMESPACE_CONN_STRING= 35 | AZURE_EVENT_HUBS_CHECKPOINT_STORE_CONN_STRING= 36 | AZURE_EVENT_HUBS_BLOB_CONTAINER_NAME=event-hubs-checkpoint-store 37 | 38 | # Jobs 39 | JOBS_SCHEDULER_TYPE=memory 40 | JOBS_SCHEDULER_DSN=postgresql://:@:/shipping 41 | 42 | # Slack 43 | SLACK_TOKEN=fill-in-your-slack-token-here 44 | 45 | # Logging 46 | LOG_ENABLED=true 47 | 48 | LOG_KIBANA_ENABLED=false 49 | LOG_KIBANA_URL=https://es.logs.local.shipping.com:443 50 | LOG_KIBANA_USERNAME= 51 | LOG_KIBANA_PASSWORD= 52 | 53 | LOG_SLACK_ENABLED=false 54 | LOG_SLACK_CHANNEL_ERRORS=shipping-errors 55 | 56 | # APIs 57 | BASE_URL=http://localhost:8010/shipping 58 | 59 | # Tasks 60 | TASK_RUNNER_USERNAME=task-runner@example.com 61 | TASK_RUNNER_PASSWORD=fill-in-your-secure-password-here 62 | -------------------------------------------------------------------------------- /templates/project/domain/env.test: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/templates/project/domain/env.test -------------------------------------------------------------------------------- /templates/project/domain/env.test_pipeline: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/templates/project/domain/env.test_pipeline -------------------------------------------------------------------------------- /templates/project/domain/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/templates/project/domain/requirements.txt -------------------------------------------------------------------------------- /templates/project/domain/src/domain/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/templates/project/domain/src/domain/__init__.py -------------------------------------------------------------------------------- /templates/project/domain/src/domain/adapters/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/templates/project/domain/src/domain/adapters/__init__.py -------------------------------------------------------------------------------- /templates/project/domain/src/domain/adapters/http/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/templates/project/domain/src/domain/adapters/http/__init__.py -------------------------------------------------------------------------------- /templates/project/domain/src/domain/adapters/listeners/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/templates/project/domain/src/domain/adapters/listeners/__init__.py -------------------------------------------------------------------------------- /templates/project/domain/src/domain/adapters/listeners/domain/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/templates/project/domain/src/domain/adapters/listeners/domain/__init__.py -------------------------------------------------------------------------------- /templates/project/domain/src/domain/adapters/listeners/interchange/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/templates/project/domain/src/domain/adapters/listeners/interchange/__init__.py -------------------------------------------------------------------------------- /templates/project/domain/src/domain/application/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/templates/project/domain/src/domain/application/__init__.py -------------------------------------------------------------------------------- /templates/project/domain/src/domain/application/config.py: -------------------------------------------------------------------------------- 1 | from ddd.application.config import Config as BaseConfig 2 | 3 | 4 | class Config(BaseConfig): 5 | def __init__(self): 6 | super().__init__() 7 | -------------------------------------------------------------------------------- /templates/project/domain/src/domain/application/domain_application_service.py: -------------------------------------------------------------------------------- 1 | from ddd.application.application_service import ApplicationService 2 | from ddd.application.action import action 3 | 4 | 5 | class ShippingApplicationService(ApplicationService): 6 | """ 7 | This is the context's application service. 8 | """ 9 | def __init__( 10 | self, 11 | customer_repository, 12 | db_service, 13 | domain_adapter, 14 | domain_publisher, 15 | event_repository, 16 | interchange_adapter, 17 | interchange_publisher, 18 | job_adapter, 19 | job_service, 20 | log_service, 21 | scheduler_adapter, 22 | shipment_repository, 23 | max_concurrent_actions, 24 | loop, 25 | ): 26 | """ 27 | Initialize the application service. 28 | """ 29 | super().__init__( 30 | db_service=db_service, 31 | domain_adapter=domain_adapter, 32 | domain_publisher=domain_publisher, 33 | event_repository=event_repository, 34 | interchange_adapter=interchange_adapter, 35 | interchange_publisher=interchange_publisher, 36 | job_adapter=job_adapter, 37 | job_service=job_service, 38 | log_service=log_service, 39 | scheduler_adapter=scheduler_adapter, 40 | max_concurrent_actions=max_concurrent_actions, 41 | loop=loop, 42 | ) 43 | 44 | # References 45 | self.customer_repository = customer_repository 46 | self.shipment_repository = shipment_repository 47 | 48 | # Classify deps 49 | self.domain_services.extend([ 50 | 51 | ]) 52 | 53 | self.primary_adapters.extend([ 54 | 55 | ]) 56 | 57 | self.secondary_adapters.extend([ 58 | customer_repository, 59 | shipment_repository, 60 | ]) 61 | 62 | # Actions 63 | 64 | @action 65 | async def send_tracking_email(self, command, corr_ids=None): 66 | """ 67 | Send tracking email to recipient. 68 | """ 69 | corr_ids = corr_ids if corr_ids is not None else [] 70 | 71 | raise NotImplementedError() 72 | -------------------------------------------------------------------------------- /templates/project/domain/src/domain/domain/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/templates/project/domain/src/domain/domain/__init__.py -------------------------------------------------------------------------------- /templates/project/domain/src/domain/domain/commands.py: -------------------------------------------------------------------------------- 1 | """ 2 | The domain commands. 3 | """ 4 | from ddd.domain.command import Command 5 | 6 | 7 | # Actions 8 | 9 | class SendTrackingEmailCommand(Command): 10 | def __init__( 11 | self, 12 | shipment_id, 13 | token, 14 | ): 15 | super().__init__() 16 | self.shipment_id = shipment_id 17 | self.token = token 18 | -------------------------------------------------------------------------------- /templates/project/domain/src/domain/domain/foo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/templates/project/domain/src/domain/domain/foo/__init__.py -------------------------------------------------------------------------------- /templates/project/domain/src/domain/domain/foo/foo.py: -------------------------------------------------------------------------------- 1 | from ddd.domain.aggregate import Aggregate 2 | 3 | 4 | class Customer(Aggregate): 5 | """ 6 | A customer. 7 | """ 8 | VERSION = "1" 9 | 10 | def __init__( 11 | self, 12 | version, 13 | customer_id, 14 | created_at, 15 | updated_at, 16 | email, 17 | first_name, 18 | last_name, 19 | ): 20 | super().__init__(version=version) 21 | self.customer_id = customer_id 22 | self.created_at = created_at 23 | self.updated_at = updated_at 24 | self.email = email 25 | self.first_name = first_name 26 | self.last_name = last_name 27 | -------------------------------------------------------------------------------- /templates/project/domain/src/domain/domain/foo/foo_id.py: -------------------------------------------------------------------------------- 1 | from ddd.domain.entity_id import EntityId 2 | 3 | 4 | class CustomerId(EntityId): 5 | """ 6 | A customer ID. 7 | """ 8 | def __init__(self, identity): 9 | super().__init__(identity=identity) 10 | -------------------------------------------------------------------------------- /templates/project/domain/src/domain/domain/foo/foo_repository.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | 3 | 4 | class CustomerRepository(object, metaclass=ABCMeta): 5 | def __init__( 6 | self, 7 | ): 8 | CustomerRepository.__init__(self) 9 | 10 | # Operations 11 | 12 | @abstractmethod 13 | async def get(self, customer_id): 14 | pass 15 | 16 | @abstractmethod 17 | async def save(self, customer): 18 | pass 19 | 20 | @abstractmethod 21 | async def delete(self, customer): 22 | pass 23 | 24 | @abstractmethod 25 | async def get_with_id( 26 | self, 27 | customer_id, 28 | ): 29 | pass 30 | 31 | @abstractmethod 32 | async def get_with_ids( 33 | self, 34 | customer_ids, 35 | ): 36 | pass 37 | -------------------------------------------------------------------------------- /templates/project/domain/src/domain/domain/foo/foo_translator.py: -------------------------------------------------------------------------------- 1 | from ddd.adapters.message_reader import MessageReader 2 | 3 | from ddd.utils.utils import str_or_none 4 | from ddd.utils.utils import iso_or_none 5 | 6 | 7 | class CustomerTranslator(object): 8 | def __init__(self, customer): 9 | super().__init__() 10 | self.customer = customer 11 | 12 | def to_domain(self): 13 | return self.to_dict() 14 | 15 | @classmethod 16 | def from_domain(cls, the_dict): 17 | from shipping.domain.customer.customer import Customer 18 | from shipping.domain.customer.customer_id import CustomerId 19 | 20 | reader = MessageReader(the_dict) 21 | 22 | # Read 23 | customer = Customer( 24 | version=reader.string_value('version'), 25 | customer_id=reader.entity_id_value('customer_id', CustomerId), 26 | created_at=reader.date_value('created_at'), 27 | updated_at=reader.date_value('updated_at'), 28 | email=reader.string_value('email'), 29 | first_name=reader.string_value('first_name'), 30 | last_name=reader.string_value('last_name'), 31 | ) 32 | 33 | return customer 34 | 35 | def to_record(self): 36 | return self.to_dict() 37 | 38 | @classmethod 39 | def from_record(cls, record): 40 | the_dict = record['data'] 41 | the_dict['customer_id'] = record['id'] 42 | 43 | return CustomerTranslator.from_domain(the_dict) 44 | 45 | def to_dict(self): 46 | return { 47 | 'version': self.customer.version, 48 | 'user_id': str_or_none(self.customer.customer_id), 49 | 'created_at': iso_or_none(self.customer.created_at), 50 | 'updated_at': iso_or_none(self.customer.updated_at), 51 | 'email': self.customer.email, 52 | 'first_name': self.customer.first_name, 53 | 'last_name': self.customer.last_name, 54 | } 55 | -------------------------------------------------------------------------------- /templates/project/domain/src/domain/repositories/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/templates/project/domain/src/domain/repositories/__init__.py -------------------------------------------------------------------------------- /templates/project/domain/src/domain/repositories/memory/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/templates/project/domain/src/domain/repositories/memory/__init__.py -------------------------------------------------------------------------------- /templates/project/domain/src/domain/repositories/memory/memory_foo_repository.py: -------------------------------------------------------------------------------- 1 | from ddd.repositories.memory.memory_repository import MemoryRepository 2 | 3 | from shipping.domain.customer.customer import Customer 4 | from shipping.domain.customer.customer_repository import CustomerRepository 5 | from shipping.domain.customer.customer_translator import CustomerTranslator 6 | 7 | 8 | class MemoryCustomerRepository(CustomerRepository, MemoryRepository): 9 | 10 | def __init__(self, log_service): 11 | MemoryRepository.__init__( 12 | self, 13 | log_service=log_service, 14 | aggregate_name="customer", 15 | aggregate_cls=Customer, 16 | translator_cls=CustomerTranslator, 17 | ) 18 | 19 | async def get(self, customer_id): 20 | return await super()._get(customer_id) 21 | 22 | async def save(self, customer): 23 | await super()._save( 24 | aggregate_id=customer.customer_id, 25 | aggregate=customer, 26 | ) 27 | 28 | async def delete(self, customer): 29 | await self._delete(customer.customer_id) 30 | 31 | async def get_with_id( 32 | self, 33 | customer_id, 34 | ): 35 | customers = await self.get_with_ids(customer_ids=[customer_id]) 36 | 37 | if len(customers): 38 | return customers[0] 39 | 40 | return None 41 | 42 | async def get_with_ids( 43 | self, 44 | customer_ids, 45 | ): 46 | records = [ 47 | r for r in self.records.values() 48 | if r['data']['customer_id'] in customer_ids 49 | ] 50 | 51 | records = self._migrate(records, self.aggregate_cls) 52 | 53 | aggregates = [ 54 | self.aggregate_from_record(record) 55 | for record in records 56 | ] 57 | 58 | return aggregates 59 | -------------------------------------------------------------------------------- /templates/project/domain/src/domain/repositories/postgres/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/templates/project/domain/src/domain/repositories/postgres/__init__.py -------------------------------------------------------------------------------- /templates/project/domain/src/domain/repositories/postgres/postgres_foo_repository.py: -------------------------------------------------------------------------------- 1 | from ddd.repositories.postgres.postgres_repository import PostgresRepository 2 | 3 | from shipping.domain.customer.customer import Customer 4 | from shipping.domain.customer.customer_repository import CustomerRepository 5 | from shipping.domain.customer.customer_translator import CustomerTranslator 6 | 7 | 8 | class PostgresCustomerRepository(CustomerRepository, PostgresRepository): 9 | def __init__( 10 | self, 11 | config, 12 | db_service, 13 | log_service, 14 | loop=None, 15 | ): 16 | PostgresRepository.__init__( 17 | self, 18 | loop=loop, 19 | db_service=db_service, 20 | log_service=log_service, 21 | aggregate_cls=Customer, 22 | translator_cls=CustomerTranslator, 23 | table_name="webshop_customers", 24 | dsn=config.database.postgres.dsn, 25 | ) 26 | 27 | # Operations 28 | 29 | async def get(self, customer_id): 30 | return await self._get(aggregate_id=customer_id) 31 | 32 | async def save(self, customer): 33 | await self._save( 34 | aggregate_id=customer.customer_id, 35 | aggregate=customer, 36 | ) 37 | 38 | async def delete(self, customer): 39 | await self._delete(aggregate_id=customer.customer_id) 40 | 41 | async def get_with_id( 42 | self, 43 | customer_id, 44 | ): 45 | customers = await self.get_with_ids(customer_ids=[customer_id]) 46 | 47 | if len(customers): 48 | return customers[0] 49 | 50 | return None 51 | 52 | async def get_with_ids( 53 | self, 54 | customer_ids, 55 | ): 56 | async with self.db_service.conn_pool.acquire() as conn: 57 | records = await conn.fetch( 58 | """SELECT * FROM {}""". 59 | format( 60 | self.table_name 61 | ) 62 | ) 63 | 64 | records = self._migrate(records, Customer) 65 | 66 | records = \ 67 | self._filter_by_property( 68 | records=records, 69 | property="customer_id", 70 | values=customer_ids, 71 | ) 72 | 73 | aggregates = [ 74 | self.aggregate_from_record(record) 75 | for record in records 76 | ] 77 | 78 | aggregates.sort(key=lambda r: r.created_at, reverse=True) 79 | 80 | return aggregates 81 | -------------------------------------------------------------------------------- /templates/project/domain/src/domain/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/templates/project/domain/src/domain/tests/__init__.py -------------------------------------------------------------------------------- /templates/project/domain/src/domain/tests/integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/templates/project/domain/src/domain/tests/integration/__init__.py -------------------------------------------------------------------------------- /templates/project/domain/src/domain/tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/templates/project/domain/src/domain/tests/unit/__init__.py -------------------------------------------------------------------------------- /templates/project/domain/src/domain/tests/unit/application/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/templates/project/domain/src/domain/tests/unit/application/__init__.py -------------------------------------------------------------------------------- /templates/project/domain/src/domain/tests/unit/application/actions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/templates/project/domain/src/domain/tests/unit/application/actions/__init__.py -------------------------------------------------------------------------------- /templates/project/domain/src/domain/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/templates/project/domain/src/domain/utils/__init__.py -------------------------------------------------------------------------------- /templates/project/domain/src/domain/utils/tasks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runemalm/ddd-for-python/502f3ce6ee4a6aec8ec131aabf992beda6774544/templates/project/domain/src/domain/utils/tasks/__init__.py -------------------------------------------------------------------------------- /templates/project/domain/src/domain/utils/tasks/seed_local_env.py: -------------------------------------------------------------------------------- 1 | from ddd.utils.tasks.task import Task as BaseTask 2 | 3 | 4 | class Task(BaseTask): 5 | 6 | def __init__(self, config, deps_mgr, args_str): 7 | super().__init__( 8 | config=config, 9 | deps_mgr=deps_mgr, 10 | args_str=args_str, 11 | makes_requests=False, 12 | ) 13 | 14 | def add_args(self, parser): 15 | parser.add_argument( 16 | "--dummy", 17 | help="a dummy argument, ignored", 18 | type=str, 19 | ) 20 | 21 | async def run(self): 22 | raise NotImplementedError() 23 | -------------------------------------------------------------------------------- /templates/project/domain/src/main.py: -------------------------------------------------------------------------------- 1 | from ddd.application.config import Config 2 | from ddd.infrastructure.container import Container 3 | 4 | from shipping.utils.dep_mgr import DependencyManager 5 | from shipping.application.shipping_application_service import \ 6 | ShippingApplicationService 7 | 8 | 9 | if __name__ == "__main__": 10 | """ 11 | This is the container entry point. 12 | Creates the app and runs it in the container. 13 | """ 14 | 15 | # Config 16 | config = Config() 17 | 18 | # Dependency manager 19 | dep_mgr = \ 20 | DependencyManager( 21 | config=config, 22 | ) 23 | 24 | # Application service 25 | service = \ 26 | ShippingApplicationService( 27 | customer_repository=dep_mgr.get_customer_repository(), 28 | db_service=dep_mgr.get_db_service(), 29 | domain_adapter=dep_mgr.get_domain_adapter(), 30 | domain_publisher=dep_mgr.get_domain_publisher(), 31 | event_repository=dep_mgr.get_event_repository(), 32 | interchange_adapter=dep_mgr.get_interchange_adapter(), 33 | interchange_publisher=dep_mgr.get_interchange_publisher(), 34 | job_adapter=dep_mgr.get_job_adapter(), 35 | job_service=dep_mgr.get_job_service(), 36 | log_service=dep_mgr.get_log_service(), 37 | scheduler_adapter=dep_mgr.get_scheduler_adapter(), 38 | shipment_repository=dep_mgr.get_shipment_repository(), 39 | max_concurrent_actions=config.max_concurrent_actions, 40 | loop=config.loop.instance, 41 | ) 42 | 43 | # ..register 44 | dep_mgr.set_service(service) 45 | 46 | # Container 47 | container = \ 48 | Container( 49 | app_service=service, 50 | log_service=dep_mgr.get_log_service(), 51 | ) 52 | 53 | # ..run 54 | loop = config.loop.instance 55 | loop.run_until_complete(container.run()) 56 | loop.close() 57 | -------------------------------------------------------------------------------- /templates/project/domain/src/run_task.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import asyncio 3 | import importlib 4 | import os 5 | 6 | from ddd.utils.utils import load_env_file 7 | 8 | 9 | def _parse_args(): 10 | """ 11 | Parse the CLI arguments. 12 | """ 13 | parser = argparse.ArgumentParser() 14 | 15 | parser.add_argument("task", help="the filename of the task", type=str) 16 | parser.add_argument("--args", help="the task arguments", type=str) 17 | 18 | args = parser.parse_args() 19 | 20 | return args.task, args.args 21 | 22 | def read_config(): 23 | """ 24 | Read config dict using 'read_config()' in utils. 25 | 26 | NOTE: 27 | 'TOP_LEVEL_PACKAGE_NAME' needs to be defined in the env file. 28 | """ 29 | path = f"{os.getenv('TOP_LEVEL_PACKAGE_NAME')}.utils.utils" 30 | 31 | module = importlib.import_module(path, package=None) 32 | 33 | func = getattr(module, "read_config") 34 | 35 | config = func() 36 | 37 | return config 38 | 39 | def find_task_class(config, task_name): 40 | 41 | # Search in project's 'tasks' folder 42 | try: 43 | path = f"{config.top_level_package_name}.utils.tasks.{task_name}" 44 | module = importlib.import_module(path, package=None) 45 | task_class = getattr(module, "Task") 46 | except ModuleNotFoundError: 47 | 48 | # Search in ddd lib's 'tasks' folder 49 | module = importlib.import_module(f"ddd.utils.tasks.{task_name}", package=None) 50 | task_class = getattr(module, "Task") 51 | 52 | return task_class 53 | 54 | 55 | if __name__ == "__main__": 56 | """ 57 | Run the task. 58 | """ 59 | # Config 60 | load_env_file() 61 | config = read_config() 62 | 63 | # Get dependency manager 64 | path = f"{config.top_level_package_name}.utils.dep_mgr" 65 | 66 | module = importlib.import_module(path, package=None) 67 | 68 | mgr_class = getattr(module, "DependencyManager") 69 | 70 | deps_mgr = \ 71 | mgr_class( 72 | config=config, 73 | ) 74 | 75 | # Args 76 | task, args = _parse_args() 77 | 78 | # Get task 79 | task_class = find_task_class(config=config, task_name=task) 80 | 81 | # Run 82 | task = \ 83 | task_class( 84 | config=config, 85 | deps_mgr=deps_mgr, 86 | args_str=args, 87 | ) 88 | 89 | asyncio.get_event_loop().run_until_complete( 90 | task._run() 91 | ) 92 | --------------------------------------------------------------------------------