From b2f4cded70eae835cb640950a325e70daf9abeea Mon Sep 17 00:00:00 2001 From: Subhash Bhushan Date: Mon, 29 Apr 2024 06:36:50 -0700 Subject: [PATCH] Add 'Compose a domain' docs section --- CHANGELOG.rst | 1 + .../compose-a-domain/activate-domain.md | 51 ++++++++ .../compose-a-domain/element-decorators.md | 113 ++++++++++++++++++ docs/guides/compose-a-domain/index.md | 63 ++++++++++ .../compose-a-domain/initialize-domain.md | 54 +++++++++ docs/guides/compose-a-domain/object-model.md | 30 +++++ .../compose-a-domain/register-elements.md | 45 +++++++ .../compose-a-domain/when-to-compose.md | 21 ++++ docs/guides/getting-started/installation.md | 73 +++++++++++ docs/intro/features.md | 22 ++++ docs/intro/philosophy.md | 87 ++++++++++---- docs/intro/whitepaper.md | 2 + docs_src/guides/composing-a-domain/001.py | 3 + docs_src/guides/composing-a-domain/002.py | 11 ++ docs_src/guides/composing-a-domain/003.py | 17 +++ docs_src/guides/composing-a-domain/004.py | 23 ++++ docs_src/guides/composing-a-domain/005.py | 37 ++++++ docs_src/guides/composing-a-domain/006.py | 10 ++ docs_src/guides/composing-a-domain/007.py | 23 ++++ docs_src/guides/composing-a-domain/008.py | 34 ++++++ docs_src/guides/composing-a-domain/009.py | 23 ++++ docs_src/guides/composing-a-domain/010.py | 32 +++++ docs_src/guides/composing-a-domain/011.py | 27 +++++ docs_src/guides/composing-a-domain/012.py | 20 ++++ docs_src/guides/composing-a-domain/013.py | 24 ++++ docs_src/guides/composing-a-domain/014.py | 13 ++ docs_src/guides/composing-a-domain/015.py | 16 +++ docs_src/guides/composing-a-domain/016.py | 33 +++++ docs_src/guides/composing-a-domain/017.py | 11 ++ docs_src/guides/composing-a-domain/018.py | 20 ++++ docs_src/guides/composing-a-domain/019.py | 40 +++++++ docs_src/guides/composing-a-domain/020.py | 17 +++ docs_src/guides/composing-a-domain/021.py | 14 +++ mkdocs.yml | 26 ++-- poetry.lock | 48 +++++++- pyproject.toml | 1 + 36 files changed, 1049 insertions(+), 36 deletions(-) create mode 100644 docs/guides/compose-a-domain/activate-domain.md create mode 100644 docs/guides/compose-a-domain/element-decorators.md create mode 100644 docs/guides/compose-a-domain/index.md create mode 100644 docs/guides/compose-a-domain/initialize-domain.md create mode 100644 docs/guides/compose-a-domain/object-model.md create mode 100644 docs/guides/compose-a-domain/register-elements.md create mode 100644 docs/guides/compose-a-domain/when-to-compose.md create mode 100644 docs/guides/getting-started/installation.md create mode 100644 docs/intro/features.md create mode 100644 docs/intro/whitepaper.md create mode 100644 docs_src/guides/composing-a-domain/001.py create mode 100644 docs_src/guides/composing-a-domain/002.py create mode 100644 docs_src/guides/composing-a-domain/003.py create mode 100644 docs_src/guides/composing-a-domain/004.py create mode 100644 docs_src/guides/composing-a-domain/005.py create mode 100644 docs_src/guides/composing-a-domain/006.py create mode 100644 docs_src/guides/composing-a-domain/007.py create mode 100644 docs_src/guides/composing-a-domain/008.py create mode 100644 docs_src/guides/composing-a-domain/009.py create mode 100644 docs_src/guides/composing-a-domain/010.py create mode 100644 docs_src/guides/composing-a-domain/011.py create mode 100644 docs_src/guides/composing-a-domain/012.py create mode 100644 docs_src/guides/composing-a-domain/013.py create mode 100644 docs_src/guides/composing-a-domain/014.py create mode 100644 docs_src/guides/composing-a-domain/015.py create mode 100644 docs_src/guides/composing-a-domain/016.py create mode 100644 docs_src/guides/composing-a-domain/017.py create mode 100644 docs_src/guides/composing-a-domain/018.py create mode 100644 docs_src/guides/composing-a-domain/019.py create mode 100644 docs_src/guides/composing-a-domain/020.py create mode 100644 docs_src/guides/composing-a-domain/021.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 651387ee..5b1c2dcc 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,7 @@ DEV * Switch from Copier to Typer and add comprehensive tests for project generation * Switch docs to Material for MkDocs and host on https://docs.proteanhq.com +* Initialize domain name to domain's module name when not provided 0.11.0 ------ diff --git a/docs/guides/compose-a-domain/activate-domain.md b/docs/guides/compose-a-domain/activate-domain.md new file mode 100644 index 00000000..2faa0cc1 --- /dev/null +++ b/docs/guides/compose-a-domain/activate-domain.md @@ -0,0 +1,51 @@ +# Activate the domain + +A `Domain` in protean is always associated with a domain context, which can be +used to bind an domain object implicitly to the current thread or greenlet. We +refer to the act of binding the domain object as **activating the domain**. + + +A `DomainContext` helps manage the active domain object for the duration of a +thread's execution. It also provides a namespace for storing data for the +duration a domain context is active. + +You activate a domain by pushing up its context to the top of the domain stack. + +## Using a Context Manager +Protean provides a helpful context manager to nest the domain operations +under. + +```Python hl_lines="18-21" +{! docs_src/guides/composing-a-domain/018.py !} +``` + +Subsequent calls to `current_domain` will return the currently active domain. +Once the task has been completed, the domain stack is reset to its original +state after popping the context. + +This is a convenient pattern to use in conjunction with most API frameworks. +The domain’s context is pushed up at the beginning of a request and popped out +once the request is processed. + +## Without the Context Manager + +You can also activate the context manually by using the `push` and `pop` +methods of the domain context: + +```python +context = domain.domain_context() + +# Activate the domain +context.push() + +# Do something interesting +# ... +# ... + +# Reset domain stack when done +context.pop() +``` + +!!! warning + If you do activate context manually, ensure you call context.pop() once the + task has been completed to prevent context leakage across threads. diff --git a/docs/guides/compose-a-domain/element-decorators.md b/docs/guides/compose-a-domain/element-decorators.md new file mode 100644 index 00000000..041be473 --- /dev/null +++ b/docs/guides/compose-a-domain/element-decorators.md @@ -0,0 +1,113 @@ +# Decorators + +Protean provides decorators to help you construct elements of your domain model. +Below is a sneak-preview into the various domain elements supported by Protean. +Each element is explored in detail in its own section. + +## `Domain.aggregate` + +```Python hl_lines="7-11" +{! docs_src/guides/composing-a-domain/002.py !} +``` + + +Read more at Aggregates. + +## `Domain.entity` + +```Python hl_lines="14-17" +{! docs_src/guides/composing-a-domain/003.py !} +``` + + +Read more at Entities. + +## `Domain.value_object` + +```Python hl_lines="7-15 23" +{! docs_src/guides/composing-a-domain/004.py !} +``` + + +Read more at Value Objects. + +## `Domain.domain_service` + +```Python hl_lines="33-37" +{! docs_src/guides/composing-a-domain/005.py !} +``` + + +Read more at Domain Services. + +## `Domain.event_sourced_aggregate` + +```Python hl_lines="7-10" +{! docs_src/guides/composing-a-domain/006.py !} +``` + + +Read more at Event Sourced Aggregates. + +## `Domain.command` + +```Python hl_lines="18-23" +{! docs_src/guides/composing-a-domain/007.py !} +``` + + +Read more at Commands. + +## `Domain.command_handler` + +```Python hl_lines="26-34" +{! docs_src/guides/composing-a-domain/008.py !} +``` + + +Read more at Command Handlers. + +## `Domain.event` + +```Python hl_lines="18-23" +{! docs_src/guides/composing-a-domain/009.py !} +``` + + +Read more at Events. + +## `Domain.event_handler` + +```Python hl_lines="28-32" +{! docs_src/guides/composing-a-domain/010.py !} +``` + + +Read more at Event Handlers. + +## `Domain.model` + +```Python hl_lines="18-25" +{! docs_src/guides/composing-a-domain/011.py !} +``` + + +Read more at Models. + +## `Domain.repository` + +```Python hl_lines="17-22" +{! docs_src/guides/composing-a-domain/012.py !} +``` + + +Read more at Repositories. + +## `Domain.view` + +```Python hl_lines="20-24" +{! docs_src/guides/composing-a-domain/013.py !} +``` + + +Read more at Views. \ No newline at end of file diff --git a/docs/guides/compose-a-domain/index.md b/docs/guides/compose-a-domain/index.md new file mode 100644 index 00000000..fe8d1bc6 --- /dev/null +++ b/docs/guides/compose-a-domain/index.md @@ -0,0 +1,63 @@ +# Compose a Domain + +The `protean.Domain` class acts as the **Composition Root** of a domain and +composes all domain elements together. It is responsible for creating and +maintaining the object graph of all the domain elements in the Bounded Context. + +`Domain` class is the one-stop gateway to: +- Register domain elements +- Retrieve dynamically-constructed artifacts like repositories and models +- Access injected technology components at runtime + +!!! note + A **domain** here is sometimes also referred to as the "Bounded Context", + because it is an implementation of the domain model. + +!!! info + A **Composition Root** is a unique location in the application where modules + are composed together. It's the place where we instantiate objects and + their dependencies before the actual application starts running. + +## Define domain object + +Constructing the object graph is a two-step procedure. First, you initialize a +domain object at a reasonable starting point of the application. + +```py hl_lines="3" +{! docs_src/guides/composing-a-domain/001.py !} +``` + +## Parameters + +### **`root_path`** + +The mandatory `root_path` parameter is the directory containing the domain's +elements. + +Typically, this is the folder containing the file initializing the domain +object. Protean uses this path to traverse the directory structure +and [auto-discover domain elements](#auto-discover-domain-elements) when the +domain is [initialized](#initialize-the-domain). + +In the example below, the domain is defined in `my_domain.py`. Domain elements +are nested within the `src` folder, directly or in their own folders. + +```shell +my_project +├── src +│ └── my_domain.py +│ └── authentication +│ └── user_aggregate.py +│ └── account_aggregate.py +├── pyproject.toml +├── poetry.lock +``` + + +Review the guide on structuring your domain for more information. + +### **`name`** + +The constructor also accepts an optional domain name to uniquely identify the +domain in the application ecosystem. When not provided, the name is initialized +to the name of the module defining the domain. diff --git a/docs/guides/compose-a-domain/initialize-domain.md b/docs/guides/compose-a-domain/initialize-domain.md new file mode 100644 index 00000000..ca4bf43a --- /dev/null +++ b/docs/guides/compose-a-domain/initialize-domain.md @@ -0,0 +1,54 @@ +# Initialize the domain + +The domain is initialized by calling the `init` method. + +```python +domain.init() +``` + +A call to `init` does the following: + +## 1. Traverse the domain model + +By default, Protean traverses the directory structure under the domain file +to discover domain elements. You can control this behavior with the `traverse` +flag: + +```python +domain.init(traverse=False) +``` + +If you choose to not traverse, Protean will not be able to detect domain +elements automatically. You are responsible for registering each element +with Protean explicitly. + +## 2. Construct the object graph + +Protean constructs a graph of all elements registered with a domain and +exposes them in a registry. + +```Python hl_lines="28-35" +{! docs_src/guides/composing-a-domain/016.py !} +``` + +## 3. Inject dependencies + +By default, a protean domain is configured to use in-memory replacements for +infrastructure, like databases, brokers, and caches. They are useful for +testing and prototyping. But for production purposes, you will want to choose +a database that actually persists data. + +Calling `domain.init()` establishes connectivity with the underlying infra, +testing access, and making them available for use by the rest of the system. + +```Python hl_lines="5-11" +{! docs_src/guides/composing-a-domain/017.py !} +``` + +In the example above, the domain activates an SQLite database repository and +makes it available for domain elements for further use. + + + +Refer to Configuration Handling to understand the different ways to configure +the domain. diff --git a/docs/guides/compose-a-domain/object-model.md b/docs/guides/compose-a-domain/object-model.md new file mode 100644 index 00000000..97b219fb --- /dev/null +++ b/docs/guides/compose-a-domain/object-model.md @@ -0,0 +1,30 @@ +# Object Model + +A domain model in Protean is composed with various types of domain elements, +all of which have a common structure and share a few behavioral traits. This +document outlines generic aspects that apply to every domain element. + +## `Element` Base class + +`Element` is a base class inherited by all domain elements. Currently, it does +not have any data structures or behavior associated with it. + +## Configuration Options + +Additional options can be passed to a domain element in two ways: + +- **`Meta` inner class** + +You can specify options within a nested inner class called `Meta`: + +```Python hl_lines="13-14" +{! docs_src/guides/composing-a-domain/020.py !} +``` + +- **Decorator Parameters** + +You can also pass options as parameters to the decorator: + +```Python hl_lines="7" +{! docs_src/guides/composing-a-domain/021.py !} +``` diff --git a/docs/guides/compose-a-domain/register-elements.md b/docs/guides/compose-a-domain/register-elements.md new file mode 100644 index 00000000..0f8b8495 --- /dev/null +++ b/docs/guides/compose-a-domain/register-elements.md @@ -0,0 +1,45 @@ +# Register elements + +The domain object is used by the domain's elements to register themselves with +the domain. + +## With decorators + +```Python hl_lines="7-11" +{! docs_src/guides/composing-a-domain/002.py !} +``` + +A full list of domain decorators along with examples are available in the +[decorators](element-decorators.md) section. + +## Explicit registration + +You can also choose to register elements manually. + +```Python hl_lines="7-13" +{! docs_src/guides/composing-a-domain/014.py !} +``` + +Note that the `User` class has been subclassed from `BaseAggregate`. That is +how Protean understands the kind of domain element being registered. Each type +of element in Protean has a distinct base class of its own. + + + +## Passing additional options + +There might be additional options you will pass in a `Meta` inner class, +depending upon the element being registered. + +```Python hl_lines="12-13" +{! docs_src/guides/composing-a-domain/015.py !} +``` + +In the above example, the `User` aggregate's default stream name - `user` - is +customized to `account`. + +Review the [object model](object-model.md) to understand +multiple ways to pass these options. Refer to each domain element's +documentation to understand the additional options supported by that element. + + diff --git a/docs/guides/compose-a-domain/when-to-compose.md b/docs/guides/compose-a-domain/when-to-compose.md new file mode 100644 index 00000000..4a806031 --- /dev/null +++ b/docs/guides/compose-a-domain/when-to-compose.md @@ -0,0 +1,21 @@ +# When to compose + +The `Domain` class in Protean acts as a composition root. It manages external +dependencies and injects them into objects during application startup. + +Your domain should be composed at the start of the application lifecycle. In +simple console applications, the `main` method is a good entry point. In most +web applications that spin up their own runtime, we depend on the callbacks or +hooks of the framework to compose the object graph, activate the composition +root, and inject dependencies into objects. + +Accordingly, depending on the software stack you will ultimately use, you will decide when to activate the domain. + +Below is an example of composing the domain with Flask as the API framework. +You would compose the domain along with the app object, and activate it (push +up the context) before processing a request. + + +```Python hl_lines="29 33 35 38" +{! docs_src/guides/composing-a-domain/019.py !} +``` diff --git a/docs/guides/getting-started/installation.md b/docs/guides/getting-started/installation.md new file mode 100644 index 00000000..50b4f2f7 --- /dev/null +++ b/docs/guides/getting-started/installation.md @@ -0,0 +1,73 @@ + +# Installation + +## Install Python + +!!! note + Protean supports Python 3.11 and newer, but it is recommended that that you + use the latest version of Python. + +[pyenv](https://github.com/pyenv/pyenv) allows you to install and manage +multiple versions of python. Follow pyenv's +[installation](https://github.com/pyenv/pyenv?tab=readme-ov-file#installation) +guide to install Python 3.11+. + +There are many version managers that help you create virtual environments, +but we will quickly walk through the steps to create a virtual environment with +one bundled with Python, `venv`. + +## Create an environment + +Create a project folder and a `.venv` folder within. Follow Python's +[venv guide](https://docs.python.org/3/library/venv.html) to install a new +virtual environment. + +```shell +$ python3 -m venv .venv +``` + +## Activate the environment + +```shell +$ source .venv/bin/activate +``` + +Your shell prompt will change to show the name of the activated environment. + +You can verify the Pyton version by typing ``python`` from your shell; +you should see something like +```shell +$ python --version +Python 3.11.8 +``` + +## Install Protean + +Within the activated environment, install Protean with the following command: + +```shell +$ pip install protean +``` + +## Verifying + +Use the ``protean`` CLI to verify the installation: + +```shell +$ protean --version +Protean 0.11.0 +``` + +To verify that Protean can be seen by your current installation of Python, +try importing Protean from a ``python`` shell: + +```shell +$ python +>>> import protean +>>> protean.get_version() +'0.11.0' +``` + +------------------- + +That's it! You can now get a sneak peek into Protean with a quick tutorial. diff --git a/docs/intro/features.md b/docs/intro/features.md new file mode 100644 index 00000000..0847b9d7 --- /dev/null +++ b/docs/intro/features.md @@ -0,0 +1,22 @@ +# Features + +**Protean** gives you the following: + +### Rapid Prototying + Visual representation of the domain + +### Automatic Ubiquitous Language Management + +### Scaffolding Support + +### Multi-domain Support + +### Technology-agnostic Domain Models + +### Event-centric Communication + +### Dependency Injection + +### Pluggable Adapters + + diff --git a/docs/intro/philosophy.md b/docs/intro/philosophy.md index 1f510b65..48a0a095 100644 --- a/docs/intro/philosophy.md +++ b/docs/intro/philosophy.md @@ -1,31 +1,76 @@ -# Protean Philosophy (draft) +# Philosophy -Rapid Prototying -Event centric communication -Enabling practices around strategic patterns -Visual representation of the domain -Technology-agnostic domain models +Protean is designed around the core principles and practices of +domain-driven design (DDD), enabling developers to mirror complex business +environments in code with precision and clarity. Protean advocates the +separation of domain logic from infrastructural concerns, ensuring that +developers can focus on delivering value through clear, technology-agnostic +domain models. -## Constraints +## Distill the problem domain -Constaints help give shape to the problem. +The distillation of the problem domain is at the core of Protean's approach. +Protean provides multiple mechanisms to deeply understand and reflect the +business domain in a domain model. -- CQRS pattern for the win: All workflows are based on CQRS. -- Communication is only through events -- Event models are the only way to express workflows -- BDD specs are the only way to capture requirements +Rapid Prototyping -## Distilling the Problem Domain +A visual mechanism to construct the domain model, bridging the gap between +business and technical team. It allows to take an experimental approach to +domain modeling. Multiple versions can be constructed. Domain models can be +evolved progressively. -Knowledge Crunching is the key. +The domain model as a graph with all its +elements and associations, allowing better colloboration between domain experts +and developers. -## Strategic Patterns +## Mirror the domain in code -Shine a light on aspects that are pivotal in DDD, but receive almost no -attention, like the practices surrounding ubiquitous language, analysis models, -experimentation, and technology agnostic development. +Rapid Prototyping. Protean allows you to construct your domain model independently without +worrying about underlying technology or infrastructure. It empowers you to +translate business requirements into domain elements directly, using standard +DDD constructs and patterns. -## Ports and Adapters +The domain model can then be subjected to business use cases to test for its +validity. -Avoid technology decisions and lock-ins by building to an abstraction. And then -powering the abstraction with any technlogy adapter as you see fit. \ No newline at end of file +## Remain technology agnostic + +Protean encourages building applications independent of technology constraints, +using abstractions that can be powered by a technology adapter of your choice. +This approach prevents premature lock-in to specific technologies, enhancing +the adaptability and longevity of your applications. + +The underlying infrastructure is abstracted away, and you specify your +technology choices through configuration. But there will always be fine-tuning +necessary for practical reasons, so Protean remains pragmatic and provides +escapte hatches to allow you to override its implementation and specify your +own. + +By decoupling domain logic from infrastructure, Protean helps you: + +- Accurately reflect the business needs in code with minimal translation. +- Postpone technological decisions until they become necessary and evident. +- Achieve extensive test coverage of business logic, aiming for 100% coverage. + +Infrastructure elements, whether databases, API frameworks, or message brokers, +are integrated at runtime, ensuring that your core domain logic is insulated +and remains consistent across various environments, including testing scenarios. + +## Choose the right architecture patterns + +Protean does not prescribe a one-size-fits-all solution but instead offers +the flexibility to choose and combine architectural patterns that best suit +the needs of the domain: + +- Flexibility and Choice: Developers are free to implement DDD, CRUD, CQRS, +Event Sourcing, or any combination thereof, depending on what best addresses +the problem at hand. +- Microservices Architecture: The use of microservices within Protean allows +for decentralized governance and technology diversity, which is crucial for +large-scale enterprise applications. +- Pattern Suitability: Each microservice or component can independently decide +its architectural style, promoting a system that is as heterogeneous as it +needs to be. + +## Progressive fragmentation of domain \ No newline at end of file diff --git a/docs/intro/whitepaper.md b/docs/intro/whitepaper.md new file mode 100644 index 00000000..bceb2ae9 --- /dev/null +++ b/docs/intro/whitepaper.md @@ -0,0 +1,2 @@ +# Whitepaper + diff --git a/docs_src/guides/composing-a-domain/001.py b/docs_src/guides/composing-a-domain/001.py new file mode 100644 index 00000000..eaaf70de --- /dev/null +++ b/docs_src/guides/composing-a-domain/001.py @@ -0,0 +1,3 @@ +from protean import Domain + +domain = Domain(__file__) diff --git a/docs_src/guides/composing-a-domain/002.py b/docs_src/guides/composing-a-domain/002.py new file mode 100644 index 00000000..52288a80 --- /dev/null +++ b/docs_src/guides/composing-a-domain/002.py @@ -0,0 +1,11 @@ +from protean import Domain +from protean.fields import Integer, String + +domain = Domain(__file__) + + +@domain.aggregate +class User: + first_name = String(max_length=50) + last_name = String(max_length=50) + age = Integer() diff --git a/docs_src/guides/composing-a-domain/003.py b/docs_src/guides/composing-a-domain/003.py new file mode 100644 index 00000000..6ecd9c1c --- /dev/null +++ b/docs_src/guides/composing-a-domain/003.py @@ -0,0 +1,17 @@ +from protean import Domain +from protean.fields import Integer, String + +domain = Domain(__file__) + + +@domain.aggregate +class User: + first_name = String(max_length=50) + last_name = String(max_length=50) + age = Integer() + + +@domain.entity(aggregate_cls=User) +class Credentials: + email = String(max_length=254) + password_hash = String(max_length=128) diff --git a/docs_src/guides/composing-a-domain/004.py b/docs_src/guides/composing-a-domain/004.py new file mode 100644 index 00000000..012e9d0d --- /dev/null +++ b/docs_src/guides/composing-a-domain/004.py @@ -0,0 +1,23 @@ +from protean import Domain +from protean.fields import Integer, String, ValueObject + +domain = Domain(__file__) + + +@domain.value_object +class Address: + address1 = String(max_length=255, required=True) + address2 = String(max_length=255) + address3 = String(max_length=255) + city = String(max_length=25, required=True) + state = String(max_length=25, required=True) + country = String(max_length=2, required=True) + zip = String(max_length=6, required=True) + + +@domain.aggregate +class User: + first_name = String(max_length=50) + last_name = String(max_length=50) + age = Integer() + address = ValueObject(Address) diff --git a/docs_src/guides/composing-a-domain/005.py b/docs_src/guides/composing-a-domain/005.py new file mode 100644 index 00000000..f0fcd03e --- /dev/null +++ b/docs_src/guides/composing-a-domain/005.py @@ -0,0 +1,37 @@ +from protean import Domain +from protean.fields import Identifier, Integer, String, ValueObject + +domain = Domain(__file__) + + +@domain.aggregate +class User: + first_name = String(max_length=50) + last_name = String(max_length=50) + age = Integer() + + +@domain.value_object(aggregate_cls="Subscription") +class Subscriber: + id = Identifier() + full_name = String(max_length=102) + + +@domain.aggregate +class Subscription: + plan = String(max_length=50) + user = ValueObject(Subscriber) + status = String(max_length=50) + + +@domain.aggregate +class Plan: + name = String(max_length=50) + price = Integer() + + +@domain.domain_service +class SubscriptionManagement: + def subscribe_user(self, user, plan): + subscription = Subscription(user=user, plan=plan, status="ACTIVE") + return subscription diff --git a/docs_src/guides/composing-a-domain/006.py b/docs_src/guides/composing-a-domain/006.py new file mode 100644 index 00000000..8d568f26 --- /dev/null +++ b/docs_src/guides/composing-a-domain/006.py @@ -0,0 +1,10 @@ +from protean import Domain +from protean.fields import Integer, String + +domain = Domain(__file__) + + +@domain.event_sourced_aggregate +class Person: + name = String() + age = Integer() diff --git a/docs_src/guides/composing-a-domain/007.py b/docs_src/guides/composing-a-domain/007.py new file mode 100644 index 00000000..f5fdd431 --- /dev/null +++ b/docs_src/guides/composing-a-domain/007.py @@ -0,0 +1,23 @@ +from protean import Domain +from protean.fields import Identifier, String + +domain = Domain(__file__) + + +@domain.aggregate +class User: + name = String(max_length=50) + + +@domain.entity(aggregate_cls=User) +class Credentials: + email = String(max_length=254) + password_hash = String(max_length=128) + + +@domain.command(aggregate_cls=User) +class Register: + id = Identifier() + email = String() + name = String() + password_hash = String() diff --git a/docs_src/guides/composing-a-domain/008.py b/docs_src/guides/composing-a-domain/008.py new file mode 100644 index 00000000..4cf2d5ba --- /dev/null +++ b/docs_src/guides/composing-a-domain/008.py @@ -0,0 +1,34 @@ +from protean import Domain, handle +from protean.fields import Identifier, String + +domain = Domain(__file__) + + +@domain.event_sourced_aggregate +class User: + id = Identifier() + email = String() + name = String() + + +@domain.command(aggregate_cls=User) +class Register: + user_id = Identifier() + email = String() + + +@domain.command(aggregate_cls=User) +class ChangePassword: + old_password_hash = String() + new_password_hash = String() + + +@domain.command_handler +class UserCommandHandlers: + @handle(Register) + def register(self, command: Register) -> None: + pass + + @handle(ChangePassword) + def change_password(self, command: ChangePassword) -> None: + pass diff --git a/docs_src/guides/composing-a-domain/009.py b/docs_src/guides/composing-a-domain/009.py new file mode 100644 index 00000000..9b966f33 --- /dev/null +++ b/docs_src/guides/composing-a-domain/009.py @@ -0,0 +1,23 @@ +from protean import Domain +from protean.fields import Identifier, String + +domain = Domain(__file__) + + +@domain.aggregate +class User: + name = String(max_length=50) + + +@domain.entity(aggregate_cls=User) +class Credentials: + email = String(max_length=254) + password_hash = String(max_length=128) + + +@domain.event(aggregate_cls=User) +class Registered: + id = Identifier() + email = String() + name = String() + password_hash = String() diff --git a/docs_src/guides/composing-a-domain/010.py b/docs_src/guides/composing-a-domain/010.py new file mode 100644 index 00000000..287f21f7 --- /dev/null +++ b/docs_src/guides/composing-a-domain/010.py @@ -0,0 +1,32 @@ +from protean import Domain, handle +from protean.fields import Identifier, String + +domain = Domain(__file__) + + +@domain.aggregate +class User: + id = Identifier() + email = String() + name = String() + + +@domain.command(aggregate_cls=User) +class Register: + id = Identifier() + email = String() + + +@domain.event(aggregate_cls=User) +class Registered: + id = Identifier() + email = String() + name = String() + password_hash = String() + + +@domain.event_handler +class UserEventHandlers: + @handle(Registered) + def send_email_notification(self, event: Registered) -> None: + pass diff --git a/docs_src/guides/composing-a-domain/011.py b/docs_src/guides/composing-a-domain/011.py new file mode 100644 index 00000000..ac0a94ae --- /dev/null +++ b/docs_src/guides/composing-a-domain/011.py @@ -0,0 +1,27 @@ +import uuid + +import sqlalchemy as sa + +from sqlalchemy.dialects.postgresql import UUID + +from protean import Domain +from protean.fields import Identifier, String + +domain = Domain(__file__) + + +@domain.aggregate +class User: + id = Identifier() + email = String() + name = String() + + +@domain.model(aggregate_cls=User) +class UserCustomModel: + id = sa.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + name = sa.Column(sa.String(50)) + email = sa.Column(sa.String(254)) + + class Meta: + schema_name = "customers" diff --git a/docs_src/guides/composing-a-domain/012.py b/docs_src/guides/composing-a-domain/012.py new file mode 100644 index 00000000..f3270ccd --- /dev/null +++ b/docs_src/guides/composing-a-domain/012.py @@ -0,0 +1,20 @@ +from typing import List + +from protean import Domain +from protean.fields import Integer, String +from protean.globals import current_domain + +domain = Domain(__file__) + + +@domain.aggregate +class Person: + first_name = String(max_length=50, required=True) + last_name = String(max_length=50, required=True) + age = Integer(default=21) + + +@domain.repository(aggregate_cls=Person) +class PersonCustomRepository: + def adults(self, minimum_age: int = 21) -> List[Person]: + return current_domain.repository_for(Person)._dao.filter(age__gte=minimum_age) diff --git a/docs_src/guides/composing-a-domain/013.py b/docs_src/guides/composing-a-domain/013.py new file mode 100644 index 00000000..99e46d1e --- /dev/null +++ b/docs_src/guides/composing-a-domain/013.py @@ -0,0 +1,24 @@ +from protean import Domain +from protean.fields import Identifier, Integer, String + +domain = Domain(__file__) + + +@domain.aggregate +class User: + first_name = String(max_length=50) + last_name = String(max_length=50) + age = Integer() + + +@domain.entity(aggregate_cls=User) +class Credentials: + email = String(max_length=254) + password_hash = String(max_length=128) + + +@domain.view(aggregate_cls=User) +class Token: + key = Identifier(identifier=True) + id = Identifier(required=True) + email = String(required=True) diff --git a/docs_src/guides/composing-a-domain/014.py b/docs_src/guides/composing-a-domain/014.py new file mode 100644 index 00000000..734c73d8 --- /dev/null +++ b/docs_src/guides/composing-a-domain/014.py @@ -0,0 +1,13 @@ +from protean import BaseAggregate, Domain +from protean.fields import Integer, String + +domain = Domain(__file__) + + +class User(BaseAggregate): + first_name = String(max_length=50) + last_name = String(max_length=50) + age = Integer() + + +domain.register(User) diff --git a/docs_src/guides/composing-a-domain/015.py b/docs_src/guides/composing-a-domain/015.py new file mode 100644 index 00000000..e0864052 --- /dev/null +++ b/docs_src/guides/composing-a-domain/015.py @@ -0,0 +1,16 @@ +from protean import BaseAggregate, Domain +from protean.fields import Integer, String + +domain = Domain(__file__) + + +class User(BaseAggregate): + first_name = String(max_length=50) + last_name = String(max_length=50) + age = Integer() + + class Meta: + stream_name = "account" + + +domain.register(User) diff --git a/docs_src/guides/composing-a-domain/016.py b/docs_src/guides/composing-a-domain/016.py new file mode 100644 index 00000000..f0161a51 --- /dev/null +++ b/docs_src/guides/composing-a-domain/016.py @@ -0,0 +1,33 @@ +from protean import Domain +from protean.fields import Identifier, String + +domain = Domain(__file__) + + +@domain.aggregate +class User: + name = String(max_length=50) + + +@domain.entity(aggregate_cls=User) +class Credentials: + email = String(max_length=254) + password_hash = String(max_length=128) + + +@domain.event(aggregate_cls=User) +class Registered: + id = Identifier() + email = String() + name = String() + password_hash = String() + + +print(domain.registry.elements) +""" Output: +{ + 'aggregates': [], + 'events': [], + 'entities': [] +} +""" diff --git a/docs_src/guides/composing-a-domain/017.py b/docs_src/guides/composing-a-domain/017.py new file mode 100644 index 00000000..0235a110 --- /dev/null +++ b/docs_src/guides/composing-a-domain/017.py @@ -0,0 +1,11 @@ +from protean import Domain + +domain = Domain(__file__) + +domain.config["DATABASES"]["default"] = { + "PROVIDER": "protean.adapters.repository.sqlalchemy.SAProvider", + "DATABASE": "SQLITE", + "DATABASE_URI": "sqlite:///:memory:", +} + +domain.init(traverse=False) diff --git a/docs_src/guides/composing-a-domain/018.py b/docs_src/guides/composing-a-domain/018.py new file mode 100644 index 00000000..3c495b35 --- /dev/null +++ b/docs_src/guides/composing-a-domain/018.py @@ -0,0 +1,20 @@ +from protean import Domain +from protean.fields import Integer, String +from protean.globals import current_domain + +domain = Domain(__file__) + + +@domain.aggregate +class User: + first_name = String(max_length=50) + last_name = String(max_length=50) + age = Integer() + + +domain.init(traverse=False) + + +with domain.domain_context().push(): + # Access an active, connected instance of User Repository + user_repo = current_domain.repository_for(User) diff --git a/docs_src/guides/composing-a-domain/019.py b/docs_src/guides/composing-a-domain/019.py new file mode 100644 index 00000000..14b43338 --- /dev/null +++ b/docs_src/guides/composing-a-domain/019.py @@ -0,0 +1,40 @@ +import logging.config + +from flask import Flask + +from protean import Domain +from protean.domain.context import has_domain_context +from protean.fields import Integer, String + +domain = Domain(__file__) + + +@domain.aggregate +class User: + first_name = String(max_length=50) + last_name = String(max_length=50) + age = Integer() + + +def create_app(config): + app = Flask(__name__, static_folder=None) + + domain.config.from_object(config) + logging.config.dictConfig(domain.config["LOGGING_CONFIG"]) + + domain.init() + + @app.before_request + def set_context(): + if not has_domain_context(): + # Push up a Domain Context + domain.domain_context().push() + + @app.after_request + def pop_context(response): + # Pop the Domain Context + domain.domain_context().pop() + + return response + + return app diff --git a/docs_src/guides/composing-a-domain/020.py b/docs_src/guides/composing-a-domain/020.py new file mode 100644 index 00000000..4f1e4520 --- /dev/null +++ b/docs_src/guides/composing-a-domain/020.py @@ -0,0 +1,17 @@ +from protean import Domain +from protean.fields import Integer, String + +domain = Domain(__file__) + + +@domain.aggregate +class User: + first_name = String(max_length=50) + last_name = String(max_length=50) + age = Integer() + + class Meta: + stream_name = "account" + + +domain.register(User) diff --git a/docs_src/guides/composing-a-domain/021.py b/docs_src/guides/composing-a-domain/021.py new file mode 100644 index 00000000..add24f07 --- /dev/null +++ b/docs_src/guides/composing-a-domain/021.py @@ -0,0 +1,14 @@ +from protean import Domain +from protean.fields import Integer, String + +domain = Domain(__file__) + + +@domain.aggregate(stream_name="account") +class User: + first_name = String(max_length=50) + last_name = String(max_length=50) + age = Integer() + + +domain.register(User) diff --git a/mkdocs.yml b/mkdocs.yml index 3c699d92..6fa00c4b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -37,15 +37,15 @@ theme: - content.code.select markdown_extensions: - admonition + - mdx_include - pymdownx.details - pymdownx.superfences nav: - Protean: - index.md - # - intro/philosophy.md - # - intro/features.md - # - intro/installation.md - # - intro/quickstart.md + - intro/philosophy.md + - intro/whitepaper.md + - intro/features.md - Core Concepts: # - core-concepts/index.md - core-concepts/analysis-model.md @@ -82,13 +82,19 @@ nav: # - core-concepts/event-sourcing/projections.md - Guides: - guides/index.md - # - Getting Started: + - Getting Started: # - guides/getting-started/index.md - # - guides/getting-started/installation.md - # - guides/getting-started/quickstart.md - # - Composing a Domain: - # - guides/composing-a-domain/index.md - # - guides/composing-a-domain/configuration.md + - guides/getting-started/installation.md + # - guides/getting-started/quickstart.md + - Compose a Domain: + - guides/compose-a-domain/index.md + - guides/compose-a-domain/register-elements.md + - guides/compose-a-domain/initialize-domain.md + - guides/compose-a-domain/activate-domain.md + - guides/compose-a-domain/when-to-compose.md + - guides/compose-a-domain/element-decorators.md + - guides/compose-a-domain/object-model.md + # - guides/compose-a-domain/configuration.md - Defining Domain Concepts: - guides/domain-definition/index.md - guides/domain-definition/fields.md diff --git a/poetry.lock b/poetry.lock index 16a59112..38231090 100644 --- a/poetry.lock +++ b/poetry.lock @@ -741,6 +741,17 @@ ssh = ["bcrypt (>=3.1.5)"] test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] test-randomorder = ["pytest-randomly"] +[[package]] +name = "cyclic" +version = "1.0.0" +description = "Handle cyclic relations" +optional = false +python-versions = "*" +files = [ + {file = "cyclic-1.0.0-py3-none-any.whl", hash = "sha256:32d8181d7698f426bce6f14f4c3921ef95b6a84af9f96192b59beb05bc00c3ed"}, + {file = "cyclic-1.0.0.tar.gz", hash = "sha256:ecddd56cb831ee3e6b79f61ecb0ad71caee606c507136867782911aa01c3e5eb"}, +] + [[package]] name = "decorator" version = "5.1.1" @@ -1439,6 +1450,22 @@ files = [ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] +[[package]] +name = "mdx-include" +version = "1.4.2" +description = "Python Markdown extension to include local or remote files" +optional = false +python-versions = "*" +files = [ + {file = "mdx_include-1.4.2-py3-none-any.whl", hash = "sha256:cfbeadd59985f27a9b70cb7ab0a3d209892fe1bb1aa342df055e0b135b3c9f34"}, + {file = "mdx_include-1.4.2.tar.gz", hash = "sha256:992f9fbc492b5cf43f7d8cb4b90b52a4e4c5fdd7fd04570290a83eea5c84f297"}, +] + +[package.dependencies] +cyclic = "*" +Markdown = ">=2.6" +rcslice = ">=1.1.0" + [[package]] name = "mergedeep" version = "1.3.4" @@ -1974,17 +2001,17 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pymdown-extensions" -version = "10.7.1" +version = "10.8" description = "Extension pack for Python Markdown." optional = false python-versions = ">=3.8" files = [ - {file = "pymdown_extensions-10.7.1-py3-none-any.whl", hash = "sha256:f5cc7000d7ff0d1ce9395d216017fa4df3dde800afb1fb72d1c7d3fd35e710f4"}, - {file = "pymdown_extensions-10.7.1.tar.gz", hash = "sha256:c70e146bdd83c744ffc766b4671999796aba18842b268510a329f7f64700d584"}, + {file = "pymdown_extensions-10.8-py3-none-any.whl", hash = "sha256:3539003ff0d5e219ba979d2dc961d18fcad5ac259e66c764482e8347b4c0503c"}, + {file = "pymdown_extensions-10.8.tar.gz", hash = "sha256:91ca336caf414e1e5e0626feca86e145de9f85a3921a7bcbd32890b51738c428"}, ] [package.dependencies] -markdown = ">=3.5" +markdown = ">=3.6" pyyaml = "*" [package.extras] @@ -2301,6 +2328,17 @@ prompt_toolkit = ">=2.0,<4.0" [package.extras] docs = ["Sphinx (>=3.3,<4.0)", "sphinx-autobuild (>=2020.9.1,<2021.0.0)", "sphinx-autodoc-typehints (>=1.11.1,<2.0.0)", "sphinx-copybutton (>=0.3.1,<0.4.0)", "sphinx-rtd-theme (>=0.5.0,<0.6.0)"] +[[package]] +name = "rcslice" +version = "1.1.0" +description = "Slice a list of sliceables (1 indexed, start and end index both are inclusive)" +optional = false +python-versions = "*" +files = [ + {file = "rcslice-1.1.0-py3-none-any.whl", hash = "sha256:1b12fc0c0ca452e8a9fd2b56ac008162f19e250906a4290a7e7a98be3200c2a6"}, + {file = "rcslice-1.1.0.tar.gz", hash = "sha256:a2ce70a60690eb63e52b722e046b334c3aaec5e900b28578f529878782ee5c6e"}, +] + [[package]] name = "readme-renderer" version = "43.0" @@ -3164,4 +3202,4 @@ sqlite = ["sqlalchemy"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "cd4c3b51606b8c1a81e1ac0863f9cb8b96f6e90d972232d33ccab8721fa8b03d" +content-hash = "f9e5f5df2ed6c382af0f1389593ad60a3df9d8c200b9a55bf175d4664ed11488" diff --git a/pyproject.toml b/pyproject.toml index b859fbbe..bcfe1a8b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -121,6 +121,7 @@ optional = true sphinx = ">=7.2.6" sphinx-tabs = ">=3.4.4" mkdocs-material = "^9.5.15" +mdx-include = "^1.4.2" [tool.poetry.group.types] optional = true