Skip to content

Commit

Permalink
Refactor Options to subclass dict (#456)
Browse files Browse the repository at this point in the history
`Options` is now pythonic. Even after subclassing dict, it retains attribute-style access.

This commit also contains:
- Remove `ContainerMeta` and replace with `__init_subclass__` in BaseContainer`
- Remove `BaseSerializer` functionality and associated files
- Object Model Documentation
  • Loading branch information
subhashb authored Aug 17, 2024
1 parent 9951d53 commit 0e50a2b
Show file tree
Hide file tree
Showing 25 changed files with 271 additions and 647 deletions.
2 changes: 1 addition & 1 deletion docs/guides/compose-a-domain/register-elements.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ documentation to understand the additional options supported by that element.

You can also choose to register elements manually.

```python hl_lines="7-10 13"
```python hl_lines="8-11 14"
{! docs_src/guides/composing-a-domain/014.py !}
```

Expand Down
64 changes: 47 additions & 17 deletions docs/guides/domain-definition/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,30 @@ and where any delays occur.

An event's metadata provides additional context about the event.

Sample metadata from an event:

```json
{
"id": "test::user-411b2ceb-9513-45d7-9e03-bbc0846fae93-0",
"type": "Test.UserLoggedIn.v1",
"fqn": "tests.event.test_event_metadata.UserLoggedIn",
"kind": "EVENT",
"stream": "test::user-411b2ceb-9513-45d7-9e03-bbc0846fae93",
"origin_stream": null,
"timestamp": "2024-08-16 15:30:27.977101+00:00",
"version": "v1",
"sequence_id": "0",
"payload_hash": 2438879608558888394
}
```

#### `id`

The unique identifier of the event. The event ID is a structured string, of the
format **<domain>.<aggregate>.<version>.<aggregate-id>.<sequence_id>**.
format `<domain-name>::<aggregate-name>-<aggregate-id>-<sequence_id>`.

The `id` value is simply an extension of the event's stream combined with the
`sequence_id`. Read the section on `sequence_id` to understand possible values.

#### `type`

Expand All @@ -58,31 +78,32 @@ For e.g. `Shipping.OrderShipped.v1`.

#### `fqn`

The fully qualified name of the event. This is used internally by Protean
to resconstruct objects from messages.
Internal. The fully qualified name of the event. This is used by Protean to
resconstruct objects from messages.

#### `kind`

Represents the kind of object enclosed in an event store message. Value is
`EVENT` for Events. `Metadata` class is shared between Events and Commands, so
possible values are `EVENT` and `COMMAND`.
Internal. Represents the kind of object enclosed in an event store message.
Value is `EVENT` for Events. `Metadata` class is shared between Events and
Commands, so possible values are `EVENT` and `COMMAND`.

#### `stream`

Name of the event stream. E.g. Stream `user-1234` encloses messages related to
`User` aggregate with identity `1234`.
Name of the event stream. E.g. Stream `auth::user-1234` encloses messages
related to `User` aggregate in the `Auth` domain with identity `1234`.

#### `origin_stream`

Name of the stream that originated this event or command.
Name of the stream that originated this event or command. `origin_stream` comes
handy when correlating related events or understanding causality.

#### `timestamp`

The timestamp of event generation.
The timestamp of event generation in ISO 8601 format.

#### `version`

The version of the event.
The version of the event class used to generate the event.

#### `sequence_id`

Expand All @@ -94,18 +115,26 @@ sequence ID of `1.1`, and the second update would have a sequence ID of `2.1`.
If the next update generated two events, then the sequence ID of the second
event would be `3.2`.

If the aggregate is event-sourced, the `sequence_id` is a single integer of the
position of the event in its stream.

#### `payload_hash`

The hash of the event's payload.
The `payload_hash` serves as a unique fingerprint for the event's
[payload](#payload). It is generated by hashing the stringified event payload
json with sorted keys.

`payload_hash` can be used to verify the integrity of the payload and in
implementing idempotent operations.

## Payload

The payload is a dictionary of key-value pairs that convey the information
about the event.

The payload is made available as the data in the event. If
you want to extract just the payload, you can use the `payload` property
of the event.
The payload is made available as the body of the event, which also includes
the event metadata. If you want to extract just the payload, you can use the
`payload` property of the event.

```shell hl_lines="22 24-25"
In [1]: user = User(id="1", email="<EMAIL>", name="<NAME>")
Expand Down Expand Up @@ -140,9 +169,10 @@ Out[6]: {'user_id': '1'}
Because events serve as API contracts of an aggregate with the rest of the
ecosystem, they are versioned to signal changes to contract.

By default, events have a version of **v1**.
Events have a default version of **v1**.

You can specify a version with the `__version__` class attribute:
You can override and customize the version with the `__version__` class
attribute:

```python hl_lines="3"
@domain.event(part_of=User)
Expand Down
114 changes: 73 additions & 41 deletions docs/guides/object-model.md
Original file line number Diff line number Diff line change
@@ -1,69 +1,101 @@
# Object Model

DOCUMENTATION IN PROGRESS
Domain elements in Protean have a common structure and share a few behavioral
traits.

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.
## Meta Options

## `Element` Base class
Protean elements have a `meta_` attribute that holds the configuration options
specified for the element.

`Element` is a base class inherited by all domain elements. Currently, it does
not have any data structures or behavior associated with it.
Options are passed as parameters to the element decorator:

## Element Type
```python hl_lines="7"
{! docs_src/guides/composing-a-domain/021.py !}
```

<Element>.element_type
```python
In [1]: User.meta_
Out[1]:
{'model': None,
'stream_category': 'user',
'auto_add_id_field': True,
'fact_events': False,
'abstract': False,
'schema_name': 'user',
'aggregate_cluster': User,
'is_event_sourced': False,
'provider': 'default'}
```

## Data Containers
### `abstract`

Protean provides data container elements, aligned with DDD principles to model
a domain. These containers hold the data that represents the core concepts
of the domain.
`abstract` is a common meta attribute available on all elements. An element
that is marked abstract cannot be instantiated.

There are three primary data container elements in Protean:
!!!note
Field orders are preserved in container elements.

- Aggregates: The root element that represents a consistent and cohesive
collection of related entities and value objects. Aggregates manage their
own data consistency and lifecycle.
- Entities: Unique and identifiable objects within your domain that have
a distinct lifecycle and behavior. Entities can exist independently but
are often part of an Aggregate.
- Value Objects: Immutable objects that encapsulate a specific value or
concept. They have no identity and provide a way to group related data
without independent behavior.
## Reflection

### Reflection
Protean provides reflection methods to explore container elements. Each of the
below methods accept a element or an instance of one.

### `has_fields`

Returns `True` if the element encloses fields.

## Metadata / Configuration Options
### `fields`

Additional options can be passed to a domain element in two ways:
Return a tuple of fields in the element, both explicitly defined and internally
added.

- **`Meta` inner class**
Raises `IncorrectUsageError` if called on non-container elements like
Application Services or Command Handlers.

You can specify options within a nested inner class called `Meta`:
### `declared_fields`

```python hl_lines="13-14"
{! docs_src/guides/composing-a-domain/020.py !}
```
Return a tuple of the explicitly declared fields.

- **Decorator Parameters**
### `data_fields`

You can also pass options as parameters to the decorator:
Return a tuple describing the data fields in this element. Does not include
metadata.

```python hl_lines="7"
{! docs_src/guides/composing-a-domain/021.py !}
```
Raises `IncorrectUsageError` if called on non-container elements like
Application Services or Command Handlers.

### `has_association_fields`

### `abstract`
Returns `True` if element contains associations.

### `association_fields`

Return a tuple of the association fields.

Raises `IncorrectUsageError` if called on non-container elements.

### `id_field`

Return the identity field of this element, or `None` if there is no identity
field.

### `has_id_field`

Returns `True` if the element has an identity field.

### `attributes`

Internal. Returns a dictionary of fields that generate a representation of
data for external use.

Attributes include simple field representations of complex fields like
value objects and associations.

Raises `IncorrectUsageError` if called on non-container elements

### `auto_add_id_field`
### `unique_fields`

Return fields marked as unique.

Abstract elements:
Most elements can be marked abstract to be subclassed. They cannot be instantiated
, but their field orders are preserved. Ex. Events.
Raises `IncorrectUsageError` if called on non-container elements.
5 changes: 3 additions & 2 deletions docs_src/guides/composing-a-domain/014.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from protean import BaseAggregate, Domain
from protean.core.aggregate import BaseAggregate
from protean.domain import Domain
from protean.fields import Integer, String

domain = Domain(__file__, load_toml=False)
Expand All @@ -10,4 +11,4 @@ class User(BaseAggregate):
age = Integer()


domain.register(User, stream_name="account")
domain.register(User, stream_category="account")
2 changes: 1 addition & 1 deletion docs_src/guides/composing-a-domain/015.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
domain = Domain(__file__, load_toml=False)


@domain.aggregate(stream_name="account")
@domain.aggregate(stream_category="account")
class User:
first_name = String(max_length=50)
last_name = String(max_length=50)
Expand Down
5 changes: 1 addition & 4 deletions docs_src/guides/composing-a-domain/020.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,11 @@
domain = Domain(__file__, load_toml=False)


@domain.aggregate
@domain.aggregate(stream_category="account")
class User:
first_name = String(max_length=50)
last_name = String(max_length=50)
age = Integer()

class Meta:
stream_name = "account"


domain.register(User)
2 changes: 1 addition & 1 deletion docs_src/guides/composing-a-domain/021.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
domain = Domain(__file__, load_toml=False)


@domain.aggregate(stream_name="account")
@domain.aggregate(stream_category="account")
class User:
first_name = String(max_length=50)
last_name = String(max_length=50)
Expand Down
2 changes: 1 addition & 1 deletion docs_src/guides/consume-state/001.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class Inventory:
in_stock = Integer(required=True)


@domain.event_handler(part_of=Inventory, stream_name="order")
@domain.event_handler(part_of=Inventory, stream_category="order")
class ManageInventory:
@handle(OrderShipped)
def reduce_stock_level(self, event: OrderShipped):
Expand Down
2 changes: 1 addition & 1 deletion docs_src/guides/consume-state/002.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ def adjust_stock(self, command: AdjustStock):
repository.add(product)


@domain.event_handler(stream_name="product")
@domain.event_handler(stream_category="product")
class SyncInventory:
@handle(ProductAdded)
def on_product_added(self, event: ProductAdded):
Expand Down
5 changes: 4 additions & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,13 @@ nav:
- guides/domain-behavior/aggregate-mutation.md
- guides/domain-behavior/raising-events.md
- guides/domain-behavior/domain-services.md

- Foundation:
- guides/object-model.md
- guides/identity.md
- App Layer:
- guides/configuration.md

- App Layer:
- Changing State:
- guides/change-state/index.md
- guides/change-state/commands.md
Expand Down
Loading

0 comments on commit 0e50a2b

Please sign in to comment.