skoll 0.0.1__py3-none-any.whl
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/__init__.py +0 -0
- skoll/application/__init__.py +2 -0
- skoll/application/protocols.py +46 -0
- skoll/application/types.py +123 -0
- skoll/config.py +34 -0
- skoll/domain/__init__.py +4 -0
- skoll/domain/base.py +157 -0
- skoll/domain/enums.py +17 -0
- skoll/domain/objects.py +176 -0
- skoll/domain/primitives.py +319 -0
- skoll/errors.py +141 -0
- skoll/infras/__init__.py +3 -0
- skoll/infras/mediator/__init__.py +2 -0
- skoll/infras/mediator/basic.py +60 -0
- skoll/infras/mediator/nats.py +51 -0
- skoll/infras/mediator/utils.py +71 -0
- skoll/infras/postgresql.py +154 -0
- skoll/infras/spicedb.py +183 -0
- skoll/result.py +58 -0
- skoll/utils/__init__.py +2 -0
- skoll/utils/dep_injection.py +89 -0
- skoll/utils/functional.py +169 -0
- skoll-0.0.1.dist-info/METADATA +32 -0
- skoll-0.0.1.dist-info/RECORD +27 -0
- skoll-0.0.1.dist-info/WHEEL +4 -0
- skoll-0.0.1.dist-info/entry_points.txt +2 -0
- skoll-0.0.1.dist-info/licenses/LICENSE +21 -0
skoll/__init__.py
ADDED
|
File without changes
|
|
@@ -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
|
skoll/config.py
ADDED
|
@@ -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"]))
|
skoll/domain/__init__.py
ADDED
skoll/domain/base.py
ADDED
|
@@ -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
|
skoll/domain/enums.py
ADDED
|
@@ -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"
|
skoll/domain/objects.py
ADDED
|
@@ -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())
|