skoll 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
skoll-0.0.1/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
@@ -0,0 +1 @@
1
+ 3.13
skoll-0.0.1/.secrets ADDED
@@ -0,0 +1 @@
1
+ pypi_token = pypi-AgEIcHlwaS5vcmcCJGE0M2M3ZDllLTMxYmItNGRiNC04ZWJkLTEzZGJhOTIzNzNkNgACKlszLCIyYmNkZWU4YS0wMTlkLTRjYWYtYmZjMy1hNGY1NGJjZjI5OTgiXQAABiCbq71ihIS2lGTRDgbpkhLw015JWj1px3glkhpP9JYsUw
@@ -0,0 +1,3 @@
1
+ {
2
+ "python.languageServer": "None"
3
+ }
skoll-0.0.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Monzon Diarra
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
skoll-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,32 @@
1
+ Metadata-Version: 2.4
2
+ Name: skoll
3
+ Version: 0.0.1
4
+ Summary: A simple package that provide a basic API python framework based on starlette and some domain driven design concepts
5
+ Author-email: Monzon Diarra <diarramonzon4@gmail.com>
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.13
8
+ Requires-Dist: aiohttp>=3.13.3
9
+ Requires-Dist: asyncpg>=0.31.0
10
+ Requires-Dist: attrs>=25.4.0
11
+ Requires-Dist: certifi>=2026.1.4
12
+ Requires-Dist: starlette>=0.49.3
13
+ Requires-Dist: ulid>=1.1
14
+ Description-Content-Type: text/markdown
15
+
16
+ # Skoll
17
+
18
+ A simple package that provide a basic API python framework based on starlette and some domain driven design concepts.
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ pip install skoll
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ Comming soon...
29
+
30
+ ## License
31
+
32
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
skoll-0.0.1/README.md ADDED
@@ -0,0 +1,17 @@
1
+ # Skoll
2
+
3
+ A simple package that provide a basic API python framework based on starlette and some domain driven design concepts.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install skoll
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ Comming soon...
14
+
15
+ ## License
16
+
17
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,33 @@
1
+ [project]
2
+ name = "skoll"
3
+ version = "0.0.1"
4
+ description = "A simple package that provide a basic API python framework based on starlette and some domain driven design concepts"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Monzon Diarra", email = "diarramonzon4@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.13"
10
+ dependencies = [
11
+ "aiohttp>=3.13.3",
12
+ "asyncpg>=0.31.0",
13
+ "attrs>=25.4.0",
14
+ "certifi>=2026.1.4",
15
+ "starlette>=0.49.3",
16
+ "ulid>=1.1",
17
+ ]
18
+
19
+ [project.scripts]
20
+ skoll = "skoll:main"
21
+
22
+ [build-system]
23
+ requires = ["hatchling"]
24
+ build-backend = "hatchling.build"
25
+
26
+ [tool.basedpyright]
27
+ reportAny = false
28
+ reportExplicitAny = false
29
+ reportMissingTypeStubs = false
30
+ reportUnusedCallResult = false
31
+ reportUnknownMemberType = false
32
+ typeCheckingMode = "recommended"
33
+ reportUnknownVariableType = false
File without changes
@@ -0,0 +1,2 @@
1
+ from .types import *
2
+ from .protocols import *
@@ -0,0 +1,46 @@
1
+ import typing as t
2
+ import collections.abc as c
3
+
4
+ from skoll.result import Result
5
+ from skoll.application.types import *
6
+ from skoll.domain import EntityState, Message
7
+
8
+
9
+ __all__ = ["DB", "Mediator", "Authz", "Repository"]
10
+
11
+
12
+ class Mediator(t.Protocol):
13
+
14
+ async def connect(self) -> None: ...
15
+ async def disconnect(self) -> None: ...
16
+ def register(self, *services: Service) -> None: ...
17
+ async def publish(self, msg: Message | RawMessage) -> None: ...
18
+ async def request(self, msg: Message | RawMessage) -> Result[t.Any]: ...
19
+
20
+
21
+ class Authz(t.Protocol):
22
+
23
+ async def check(self, tuple: str, cxt: dict[str, t.Any] | None = None) -> None: ...
24
+ async def write(
25
+ self, changes: list[AuthzWriteChange], preconditions: list[AuthzPrecondition] | None = None
26
+ ) -> str: ...
27
+ async def lookup(
28
+ self, filter: str, cxt: dict[str, t.Any] | None = None, limit: int | None = None, cursor: str | None = None
29
+ ) -> AuthzLookupResult: ...
30
+
31
+
32
+ class Repository[T: EntityState](t.Protocol):
33
+
34
+ async def save(self, state: T) -> None: ...
35
+ async def delete(self, criteria: Criteria) -> None: ...
36
+ async def exists(self, criteria: Criteria) -> bool: ...
37
+ async def get(self, criteria: Criteria) -> T | None: ...
38
+ async def list(self, criteria: ListCriteria) -> ListPage[T]: ...
39
+
40
+
41
+ class DB[T = t.Any](t.Protocol):
42
+
43
+ async def close(self) -> None: ...
44
+ async def connect(self) -> None: ...
45
+ def session(self) -> c.AsyncGenerator[T]: ...
46
+ def transaction(self) -> c.AsyncGenerator[T]: ...
@@ -0,0 +1,123 @@
1
+ import typing as t
2
+ import collections.abc as c
3
+ from inspect import signature
4
+ from attrs import define, field
5
+ from abc import ABC, abstractmethod
6
+
7
+ from skoll.result import Result
8
+ from skoll.errors import NotFound
9
+ from skoll.domain import DateTime, ID, Message
10
+
11
+
12
+ __all__ = [
13
+ "Service",
14
+ "Criteria",
15
+ "ListPage",
16
+ "Subscriber",
17
+ "RawMessage",
18
+ "ListCriteria",
19
+ "AuthzWriteChange",
20
+ "AuthzLookupResult",
21
+ "MissingSubscriber",
22
+ "AuthzPrecondition",
23
+ "AuthzWriteOperation",
24
+ "AuthzPreconditionOperation",
25
+ ]
26
+
27
+
28
+ type AuthzWriteOperation = t.Literal["SET", "DELETE"]
29
+ type AuthzPrecondition = tuple[AuthzPreconditionOperation, str]
30
+ type AuthzPreconditionOperation = t.Literal["MUST_MATCH", "MUST_NOT_MATCH"]
31
+ type SubscriberCallback = t.Callable[t.Concatenate[Message, ...], c.Coroutine[t.Any, t.Any, Result[t.Any]]]
32
+ type AuthzWriteChange = tuple[AuthzWriteOperation, str, tuple[str, dict[str, t.Any]] | None, DateTime | None]
33
+
34
+
35
+ class RawMessage(t.TypedDict):
36
+ topic: str
37
+ source: str
38
+ id: t.NotRequired[str]
39
+ created_at: t.NotRequired[int]
40
+ payload: t.NotRequired[dict[str, t.Any]]
41
+ context: t.NotRequired[dict[str, t.Any]]
42
+
43
+
44
+ class ListPage[T](t.NamedTuple):
45
+
46
+ items: list[T]
47
+ cursor: str | None = None
48
+
49
+
50
+ class AuthzLookupResult(t.NamedTuple):
51
+ uids: list[str]
52
+ cursor: str | None = None
53
+
54
+
55
+ class SQLCriteria(t.NamedTuple):
56
+ query: str
57
+ params: list[t.Any]
58
+
59
+
60
+ @define(frozen=True, kw_only=True, slots=True)
61
+ class Criteria(ABC):
62
+
63
+ uid: ID | None = None
64
+
65
+ @property
66
+ @abstractmethod
67
+ def as_sql(self) -> SQLCriteria:
68
+ raise NotImplementedError("Subclasses must implement this method")
69
+
70
+
71
+ @define(frozen=True, kw_only=True, slots=True)
72
+ class ListCriteria(Criteria, ABC):
73
+
74
+ cursor: str | None = None
75
+ limit: int = field(default=100)
76
+
77
+
78
+ @define(frozen=True, kw_only=True, slots=True)
79
+ class Subscriber:
80
+
81
+ topic: str
82
+ msg_arg: str
83
+ service_name: str
84
+ msg_cls: type[Message]
85
+ with_reply: bool = True
86
+ callback: SubscriberCallback
87
+
88
+
89
+ @define(kw_only=True, slots=True)
90
+ class MissingSubscriber(NotFound):
91
+
92
+ attr: str | None = field(default=None, init=False)
93
+ code: str = field(default="missing_subscriber", init=False)
94
+ detail: str = "No subscriber found for the given message subject"
95
+
96
+
97
+ @define(kw_only=True, slots=True, frozen=True)
98
+ class Service:
99
+
100
+ name: str
101
+ subscribers: list[Subscriber] = field(factory=list)
102
+
103
+ def on(self, topic: str, with_reply: bool = True):
104
+ def decorator(callback: SubscriberCallback):
105
+ first_arg = list(signature(callback).parameters.values())[0]
106
+ if not issubclass(first_arg.annotation, Message):
107
+ raise TypeError(
108
+ f"@service.on only allow on function with first argument being a subclass of Message, got {first_arg.annotation}"
109
+ )
110
+
111
+ self.subscribers.append(
112
+ Subscriber(
113
+ topic=topic,
114
+ callback=callback,
115
+ with_reply=with_reply,
116
+ msg_arg=first_arg.name,
117
+ service_name=self.name,
118
+ msg_cls=first_arg.annotation,
119
+ )
120
+ )
121
+ return callback
122
+
123
+ return decorator
@@ -0,0 +1,34 @@
1
+ from certifi import where
2
+ from attrs import define, field
3
+ from ssl import SSLContext, create_default_context
4
+
5
+ from skoll.utils import get_config_var
6
+
7
+
8
+ __all__ = ["SSLContextConfig", "SpiceDBConfig", "PostgresConfig", "NATSConfig"]
9
+
10
+
11
+ @define(frozen=True, kw_only=True)
12
+ class SSLContextConfig:
13
+
14
+ default: SSLContext = field(factory=lambda: create_default_context(cafile=where()))
15
+
16
+
17
+ @define(frozen=True, kw_only=True)
18
+ class SpiceDBConfig:
19
+
20
+ uri: str | None = field(factory=get_config_var(["SPICEDB_URI", "/run/secrets/spicedb_uri.txt"]))
21
+ token: str | None = field(factory=get_config_var(["SPICEDB_TOKEN", "/run/secrets/spicedb_token.txt"]))
22
+
23
+
24
+ @define(frozen=True, kw_only=True)
25
+ class PostgresConfig:
26
+
27
+ dsn: str | None = field(factory=get_config_var(["PG_DB_DSN", "/run/secrets/pg_db_dsn.txt"]))
28
+
29
+
30
+ @define(frozen=True, kw_only=True)
31
+ class NATSConfig:
32
+
33
+ urls: str | None = field(factory=get_config_var(["NATS_URLS", "/run/secrets/nats_urls.txt"]))
34
+ seed: str | None = field(factory=get_config_var(["NATS_SEED", "/run/secrets/nats_seed.txt"]))
@@ -0,0 +1,4 @@
1
+ from .base import *
2
+ from .enums import *
3
+ from .objects import *
4
+ from .primitives import *
@@ -0,0 +1,157 @@
1
+ import attrs
2
+ import typing as t
3
+ from abc import ABC
4
+ from types import UnionType
5
+ from enum import Enum as _Enum
6
+
7
+
8
+ from skoll.utils import to_snake_case, serialize
9
+ from skoll.errors import MissingField, InvalidField, Error
10
+ from skoll.result import Result, fail, ok, combine, is_fail, is_ok
11
+
12
+
13
+ __all__ = ["Enum", "Object"]
14
+
15
+
16
+ class Enum(_Enum):
17
+
18
+ def serialize(self) -> str:
19
+ return self.value
20
+
21
+ @classmethod
22
+ def options(cls) -> list[str]:
23
+ return [option.value for option in list(cls)]
24
+
25
+ @classmethod
26
+ def create(cls, raw: t.Any) -> Result[t.Self]:
27
+ if raw in cls.options():
28
+ return ok(cls(raw))
29
+
30
+ if raw is None:
31
+ return fail(MissingField(field=to_snake_case(cls.__name__)))
32
+
33
+ return fail(
34
+ Error(
35
+ code="unknown_option",
36
+ field=to_snake_case(cls.__name__),
37
+ hints={"expected": cls.options(), "received": raw},
38
+ detail=f"Invalid input passed, check the hints for more details.",
39
+ )
40
+ )
41
+
42
+
43
+ @attrs.define(kw_only=True, frozen=True, slots=True)
44
+ class Object(ABC):
45
+
46
+ def serialize(self) -> t.Any:
47
+ data = attrs.asdict(self)
48
+ if len(data) == 1 and data.get("value") is not None:
49
+ return serialize(data["value"])
50
+ return serialize(data)
51
+
52
+ @classmethod
53
+ def prepare(cls, raw: t.Any) -> Result[t.Any]:
54
+ return ok(raw)
55
+
56
+ @classmethod
57
+ def _init(cls, value: t.Any) -> Result[t.Self]:
58
+ if isinstance(value, dict):
59
+ return ok(cls(**value))
60
+ return ok(cls(**{"value": value}))
61
+
62
+ @classmethod
63
+ def create(cls, raw: t.Any) -> Result[t.Self]:
64
+ if raw is None:
65
+ return fail(MissingField(field=to_snake_case(cls.__name__)))
66
+
67
+ prepare_result = cls.prepare(raw)
68
+ if is_fail(prepare_result):
69
+ return prepare_result
70
+
71
+ schema = get_schema(cls)
72
+ if schema is None:
73
+ return cls._init(prepare_result.value)
74
+
75
+ results: dict[str, Result[t.Any]] = {}
76
+ for key, item in schema.items():
77
+ results[key] = item.create(prepare_result.value)
78
+ res = combine(results)
79
+ return cls._init(**res.value) if is_ok(res) else res
80
+
81
+ def evolve(self, **kwargs: t.Any) -> t.Self:
82
+ return attrs.evolve(self, **kwargs)
83
+
84
+
85
+ @attrs.define(kw_only=True, slots=True, frozen=True)
86
+ class _SchemaItem:
87
+ """
88
+ Represents a single item in object schema.
89
+ """
90
+
91
+ key: str
92
+ cls: t.Any
93
+ is_list: bool = False
94
+ optional: bool = False
95
+ default: t.Any = attrs.NOTHING
96
+
97
+ def create(self, raw: t.Any) -> Result[t.Any]:
98
+ raw_item = raw.get(self.key) if isinstance(raw, dict) else raw
99
+ if raw_item is None and self.default != attrs.NOTHING:
100
+ if not hasattr(self.default, "factory"):
101
+ return ok(value=self.default)
102
+ if self.default.takes_self is False:
103
+ return ok(value=self.default.factory())
104
+ # Ignore case where takes_self is True, as we don't have the object instance here
105
+
106
+ if self.optional is True and raw_item is None:
107
+ return ok(value=None)
108
+ if raw_item is None:
109
+ return fail(MissingField(field=self.key))
110
+ if self.is_list and isinstance(raw_item, list):
111
+ results: list[Result[t.Any]] = []
112
+ for idx, rw in enumerate(t.cast(list[t.Any], raw_item)):
113
+ res = self._create(raw=rw, field=f"{self.key}[{idx}]")
114
+ results.append(res)
115
+ res = combine(results)
116
+ if is_fail(res):
117
+ res.err.field = self.key
118
+ return res
119
+ if self.is_list and not isinstance(raw_item, list):
120
+ return fail(InvalidField(field=self.key))
121
+ return self._create(raw=raw_item)
122
+
123
+ def _create(self, raw: t.Any, field: str | None = None) -> Result[t.Any]:
124
+ field = field or self.key
125
+ if hasattr(self.cls, "create") and callable(getattr(self.cls, "create")):
126
+ return self.cls.create(raw)
127
+
128
+ type_checkers: dict[type[t.Any], t.Callable[[t.Any], bool]] = {
129
+ bool: lambda x: x in ["True", "False", True, False],
130
+ str: lambda x: isinstance(x, (str, float, int)),
131
+ int: lambda x: isinstance(x, (int, float)),
132
+ float: lambda x: isinstance(x, (float, int)),
133
+ }
134
+ check = type_checkers.get(self.cls)
135
+
136
+ if check is None or check(raw) is True:
137
+ return ok(raw if check is None else self.cls(raw))
138
+
139
+ return fail(InvalidField(field=field, hints={"expected": self.cls.__name__, "received": raw}))
140
+
141
+
142
+ def get_schema(cls: type[t.Any]) -> dict[str, _SchemaItem] | None:
143
+ if not attrs.has(cls) or (len(attrs.fields(cls)) == 1 and attrs.fields(cls)[0].name == "value"):
144
+ return None
145
+
146
+ schema: dict[str, _SchemaItem] = {}
147
+ for key, attr in attrs.fields_dict(cls).items():
148
+ is_list, optional, _cls = False, False, attr.type
149
+ if t.get_origin(_cls) == UnionType and t.get_args(_cls)[1] == type(None):
150
+ _cls = t.get_args(attr.type)[0]
151
+ optional = True
152
+ if t.get_origin(_cls) == list:
153
+ is_list = True
154
+ _cls = t.get_args(attr.type)[0]
155
+
156
+ schema[key] = _SchemaItem(key=key, cls=_cls, is_list=is_list, optional=optional, default=attr.default)
157
+ return schema
@@ -0,0 +1,17 @@
1
+ from .base import Enum
2
+
3
+ __all__ = ["SortDirection", "BaseStatus"]
4
+
5
+
6
+ class SortDirection(Enum):
7
+
8
+ ASCENDING = "ASC"
9
+ DESCENDING = "DESC"
10
+
11
+
12
+ class BaseStatus(Enum):
13
+
14
+ ACTIVE = "ACTIVE"
15
+ INACTIVE = "INACTIVE"
16
+ DELETED = "DELETED"
17
+ ARCHIVED = "ARCHIVED"
@@ -0,0 +1,176 @@
1
+ from __future__ import annotations
2
+
3
+ import typing as t
4
+ from datetime import timedelta
5
+ from attrs import define, field
6
+
7
+ from skoll.domain.base import Object
8
+ from skoll.domain.primitives import ID, PositiveInt, DateTime, Latitude, Longitude, Time, LocalizedText
9
+
10
+
11
+ __all__ = [
12
+ "Period",
13
+ "Address",
14
+ "Message",
15
+ "TimeSlot",
16
+ "Coordinate",
17
+ "EntityState",
18
+ "RegularHours",
19
+ "SpecialHours",
20
+ "WorkingHours",
21
+ "MessageContext",
22
+ "MessagePayload",
23
+ ]
24
+
25
+
26
+ @define(kw_only=True, slots=True, frozen=True)
27
+ class Coordinate(Object):
28
+
29
+ lat: Latitude
30
+ lng: Longitude
31
+
32
+ @classmethod
33
+ def from_raw(cls, lat: float, lng: float):
34
+ return Coordinate(lat=Latitude(value=lat), lng=Longitude(value=lng))
35
+
36
+
37
+ @define(kw_only=True, slots=True, frozen=True)
38
+ class Address(Object):
39
+
40
+ city: str
41
+ street: str
42
+ region: str
43
+ country: str
44
+ postal_code: str
45
+ coordinate: Coordinate
46
+
47
+
48
+ @define(kw_only=True, slots=True, frozen=True)
49
+ class Period(Object):
50
+
51
+ end: DateTime
52
+ start: DateTime
53
+
54
+ @property
55
+ def duration(self) -> timedelta:
56
+ return self.end.diff(self.start)
57
+
58
+ def days(self, tz_str: str = "UTC") -> list[DateTime]:
59
+ dates: list[DateTime] = []
60
+ date = self.start.to_tz(tz_str=tz_str).reset_part(hour=True, minute=True).reset_second()
61
+ while date < self.end.to_tz(tz_str=tz_str):
62
+ dates.append(date)
63
+ date = date.plus(days=1)
64
+
65
+ return dates
66
+
67
+
68
+ @define(kw_only=True, slots=True, frozen=True)
69
+ class TimeSlot(Object):
70
+
71
+ end: Time
72
+ start: Time
73
+
74
+ def is_between(self, hour: int, minute: int):
75
+ hours = hour + (minute / 60)
76
+ return self.start.as_hour <= hours and hours <= self.end.as_hour
77
+
78
+
79
+ @define(kw_only=True, slots=True, frozen=True)
80
+ class RegularHours(Object):
81
+
82
+ weekday: list[int] = field(factory=list)
83
+ slots: list[TimeSlot] = field(factory=list)
84
+
85
+
86
+ @define(kw_only=True, slots=True, frozen=True)
87
+ class SpecialHours(Object):
88
+
89
+ opened: bool
90
+ date: DateTime
91
+ name: LocalizedText
92
+ slots: list[TimeSlot] = field(factory=list)
93
+
94
+
95
+ @define(kw_only=True, slots=True, frozen=True)
96
+ class WorkingHours(Object):
97
+
98
+ timezone: str
99
+ always_open: bool
100
+ regular_hours: list[RegularHours] = field(factory=list)
101
+ special_hours: list[SpecialHours] = field(factory=list)
102
+
103
+
104
+ @define(kw_only=True, slots=True, frozen=True, eq=False)
105
+ class EntityState(Object):
106
+
107
+ id: ID = field(factory=ID.new)
108
+ created_at: DateTime = field(factory=DateTime.now)
109
+ updated_at: DateTime = field(factory=DateTime.now)
110
+ version: PositiveInt = field(factory=PositiveInt.zero)
111
+
112
+ @t.override
113
+ def __eq__(self, other: t.Any) -> bool:
114
+ if not isinstance(other, self.__class__):
115
+ return False
116
+ return other.__hash__() == self.__hash__()
117
+
118
+ @t.override
119
+ def __ne__(self, other: t.Any) -> bool:
120
+ return not self == other
121
+
122
+ @t.override
123
+ def __hash__(self) -> int:
124
+ return hash(self.id.serialize())
125
+
126
+ @t.override
127
+ def evolve(self, **kwargs: t.Any) -> t.Self:
128
+ if "updated_at" not in kwargs:
129
+ kwargs["updated_at"] = DateTime.now()
130
+ if "version" not in kwargs:
131
+ kwargs["version"] = self.version.increment()
132
+ return super().evolve(**kwargs)
133
+
134
+
135
+ @define(frozen=True, kw_only=True, slots=True)
136
+ class MessageContext(Object):
137
+
138
+ span_id: ID = field(factory=ID.new)
139
+ trace_id: ID = field(factory=ID.new)
140
+
141
+ @classmethod
142
+ def default(cls) -> t.Self:
143
+ return cls()
144
+
145
+
146
+ @define(frozen=True, kw_only=True, slots=True)
147
+ class MessagePayload(Object):
148
+
149
+ @classmethod
150
+ def default(cls) -> t.Self:
151
+ return cls()
152
+
153
+
154
+ @define(frozen=True, kw_only=True, slots=True, eq=False)
155
+ class Message(Object):
156
+
157
+ topic: str
158
+ source: str
159
+ id: ID = field(factory=ID.new)
160
+ created_at: DateTime = field(factory=DateTime.now)
161
+ payload: MessagePayload = field(factory=MessagePayload.default)
162
+ context: MessageContext = field(factory=MessageContext.default)
163
+
164
+ @t.override
165
+ def __eq__(self, other: t.Any) -> bool:
166
+ if not isinstance(other, self.__class__):
167
+ return False
168
+ return other.__hash__() == self.__hash__()
169
+
170
+ @t.override
171
+ def __ne__(self, other: t.Any) -> bool:
172
+ return not self == other
173
+
174
+ @t.override
175
+ def __hash__(self) -> int:
176
+ return hash(self.id.serialize())