eventsourcingdb 0.8.2__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.
- eventsourcingdb-0.8.2/LICENSE.md +21 -0
- eventsourcingdb-0.8.2/PKG-INFO +18 -0
- eventsourcingdb-0.8.2/README.md +3 -0
- eventsourcingdb-0.8.2/eventsourcingdb/__init__.py +2 -0
- eventsourcingdb-0.8.2/eventsourcingdb/abstract_base_client.py +9 -0
- eventsourcingdb-0.8.2/eventsourcingdb/client.py +93 -0
- eventsourcingdb-0.8.2/eventsourcingdb/client_configuration.py +10 -0
- eventsourcingdb-0.8.2/eventsourcingdb/client_options.py +8 -0
- eventsourcingdb-0.8.2/eventsourcingdb/errors/__init__.py +0 -0
- eventsourcingdb-0.8.2/eventsourcingdb/errors/client_error.py +6 -0
- eventsourcingdb-0.8.2/eventsourcingdb/errors/custom_error.py +2 -0
- eventsourcingdb-0.8.2/eventsourcingdb/errors/internal_error.py +6 -0
- eventsourcingdb-0.8.2/eventsourcingdb/errors/invalid_parameter_error.py +7 -0
- eventsourcingdb-0.8.2/eventsourcingdb/errors/server_error.py +6 -0
- eventsourcingdb-0.8.2/eventsourcingdb/errors/validation_error.py +6 -0
- eventsourcingdb-0.8.2/eventsourcingdb/event/__init__.py +0 -0
- eventsourcingdb-0.8.2/eventsourcingdb/event/event.py +71 -0
- eventsourcingdb-0.8.2/eventsourcingdb/event/event_candidate.py +33 -0
- eventsourcingdb-0.8.2/eventsourcingdb/event/event_context.py +120 -0
- eventsourcingdb-0.8.2/eventsourcingdb/event/source.py +30 -0
- eventsourcingdb-0.8.2/eventsourcingdb/event/validate_subject.py +13 -0
- eventsourcingdb-0.8.2/eventsourcingdb/event/validate_type.py +12 -0
- eventsourcingdb-0.8.2/eventsourcingdb/handlers/__init__.py +0 -0
- eventsourcingdb-0.8.2/eventsourcingdb/handlers/is_heartbeat.py +7 -0
- eventsourcingdb-0.8.2/eventsourcingdb/handlers/is_item.py +16 -0
- eventsourcingdb-0.8.2/eventsourcingdb/handlers/is_stream_error.py +12 -0
- eventsourcingdb-0.8.2/eventsourcingdb/handlers/observe_events/__init__.py +4 -0
- eventsourcingdb-0.8.2/eventsourcingdb/handlers/observe_events/if_event_is_missing_during_observe.py +6 -0
- eventsourcingdb-0.8.2/eventsourcingdb/handlers/observe_events/observe_events.py +85 -0
- eventsourcingdb-0.8.2/eventsourcingdb/handlers/observe_events/observe_events_options.py +55 -0
- eventsourcingdb-0.8.2/eventsourcingdb/handlers/observe_events/observe_from_latest_event.py +17 -0
- eventsourcingdb-0.8.2/eventsourcingdb/handlers/parse_raw_message.py +17 -0
- eventsourcingdb-0.8.2/eventsourcingdb/handlers/ping.py +12 -0
- eventsourcingdb-0.8.2/eventsourcingdb/handlers/read_event_types/__init__.py +0 -0
- eventsourcingdb-0.8.2/eventsourcingdb/handlers/read_event_types/event_type.py +42 -0
- eventsourcingdb-0.8.2/eventsourcingdb/handlers/read_event_types/is_event_type.py +12 -0
- eventsourcingdb-0.8.2/eventsourcingdb/handlers/read_event_types/read_event_types.py +60 -0
- eventsourcingdb-0.8.2/eventsourcingdb/handlers/read_events/__init__.py +4 -0
- eventsourcingdb-0.8.2/eventsourcingdb/handlers/read_events/if_event_is_missing_during_read.py +6 -0
- eventsourcingdb-0.8.2/eventsourcingdb/handlers/read_events/order.py +6 -0
- eventsourcingdb-0.8.2/eventsourcingdb/handlers/read_events/read_events.py +81 -0
- eventsourcingdb-0.8.2/eventsourcingdb/handlers/read_events/read_events_options.py +66 -0
- eventsourcingdb-0.8.2/eventsourcingdb/handlers/read_events/read_from_latest_event.py +17 -0
- eventsourcingdb-0.8.2/eventsourcingdb/handlers/read_subjects/__init__.py +1 -0
- eventsourcingdb-0.8.2/eventsourcingdb/handlers/read_subjects/is_subject.py +12 -0
- eventsourcingdb-0.8.2/eventsourcingdb/handlers/read_subjects/read_subjects.py +66 -0
- eventsourcingdb-0.8.2/eventsourcingdb/handlers/register_event_schema/__init__.py +0 -0
- eventsourcingdb-0.8.2/eventsourcingdb/handlers/register_event_schema/register_event_schema.py +51 -0
- eventsourcingdb-0.8.2/eventsourcingdb/handlers/store_item.py +8 -0
- eventsourcingdb-0.8.2/eventsourcingdb/handlers/write_events/__init__.py +4 -0
- eventsourcingdb-0.8.2/eventsourcingdb/handlers/write_events/preconditions.py +36 -0
- eventsourcingdb-0.8.2/eventsourcingdb/handlers/write_events/write_events.py +81 -0
- eventsourcingdb-0.8.2/eventsourcingdb/http_client/__init__.py +0 -0
- eventsourcingdb-0.8.2/eventsourcingdb/http_client/http_client.py +189 -0
- eventsourcingdb-0.8.2/eventsourcingdb/http_client/response.py +33 -0
- eventsourcingdb-0.8.2/eventsourcingdb/util/__init__.py +0 -0
- eventsourcingdb-0.8.2/eventsourcingdb/util/get_randomized_duration.py +10 -0
- eventsourcingdb-0.8.2/eventsourcingdb/util/is_non_negativ_integer.py +8 -0
- eventsourcingdb-0.8.2/eventsourcingdb/util/retry/__init__.py +0 -0
- eventsourcingdb-0.8.2/eventsourcingdb/util/retry/retry_error.py +16 -0
- eventsourcingdb-0.8.2/eventsourcingdb/util/retry/retry_result.py +17 -0
- eventsourcingdb-0.8.2/eventsourcingdb/util/retry/retry_with_backoff.py +34 -0
- eventsourcingdb-0.8.2/eventsourcingdb/util/url.py +5 -0
- eventsourcingdb-0.8.2/pyproject.toml +26 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 the native web GmbH
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: eventsourcingdb
|
|
3
|
+
Version: 0.8.2
|
|
4
|
+
Summary: eventsourcingdb is the official client for EventSourcingDB.
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: the native web GmbH
|
|
7
|
+
Author-email: hello@thenativeweb.io
|
|
8
|
+
Requires-Python: >=3.13,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Requires-Dist: aiohttp (==3.11.13)
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# eventsourcingdb
|
|
16
|
+
|
|
17
|
+
eventsourcingdb is the official client for EventSourcingDB.
|
|
18
|
+
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from collections.abc import AsyncGenerator
|
|
2
|
+
|
|
3
|
+
from .abstract_base_client import AbstractBaseClient
|
|
4
|
+
from .client_configuration import ClientConfiguration
|
|
5
|
+
from .client_options import ClientOptions
|
|
6
|
+
from .event.event_candidate import EventCandidate
|
|
7
|
+
from .event.event_context import EventContext
|
|
8
|
+
from .handlers.observe_events.observe_events import observe_events
|
|
9
|
+
from .handlers.observe_events.observe_events_options import ObserveEventsOptions
|
|
10
|
+
from .handlers.read_event_types.event_type import EventType
|
|
11
|
+
from .handlers.read_event_types.read_event_types import read_event_types
|
|
12
|
+
from .handlers.register_event_schema.register_event_schema import register_event_schema
|
|
13
|
+
from .http_client.http_client import HttpClient
|
|
14
|
+
from .handlers.ping import ping
|
|
15
|
+
from .handlers.read_events import read_events, ReadEventsOptions
|
|
16
|
+
from .handlers.read_subjects import read_subjects
|
|
17
|
+
from .handlers.store_item import StoreItem
|
|
18
|
+
from .handlers.write_events import Precondition, write_events
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# pylint: disable=R6007
|
|
22
|
+
# Reason: This class explicitly specifies the return type as None
|
|
23
|
+
# for better readability. Even though it is not necessary,
|
|
24
|
+
# it makes the return type clear without needing to read any
|
|
25
|
+
# documentation or code.
|
|
26
|
+
class Client(AbstractBaseClient):
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
base_url: str,
|
|
30
|
+
access_token: str,
|
|
31
|
+
options: ClientOptions = ClientOptions()
|
|
32
|
+
):
|
|
33
|
+
configuration = ClientConfiguration(
|
|
34
|
+
base_url=base_url,
|
|
35
|
+
timeout_seconds=options.timeout_seconds,
|
|
36
|
+
access_token=access_token,
|
|
37
|
+
protocol_version=options.protocol_version,
|
|
38
|
+
max_tries=options.max_tries
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
self.__http_client = HttpClient(configuration)
|
|
42
|
+
|
|
43
|
+
async def initialize(self) -> None:
|
|
44
|
+
await self.__http_client.initialize()
|
|
45
|
+
|
|
46
|
+
async def close(self) -> None:
|
|
47
|
+
await self.__http_client.close()
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def http_client(self) -> HttpClient:
|
|
51
|
+
return self.__http_client
|
|
52
|
+
|
|
53
|
+
async def ping(self) -> None:
|
|
54
|
+
return await ping(self)
|
|
55
|
+
|
|
56
|
+
async def read_subjects(
|
|
57
|
+
self,
|
|
58
|
+
base_subject: str
|
|
59
|
+
) -> AsyncGenerator[str, None]:
|
|
60
|
+
async for subject in read_subjects(self, base_subject):
|
|
61
|
+
yield subject
|
|
62
|
+
|
|
63
|
+
async def read_events(
|
|
64
|
+
self,
|
|
65
|
+
subject: str,
|
|
66
|
+
options: ReadEventsOptions
|
|
67
|
+
) -> AsyncGenerator[StoreItem, None]:
|
|
68
|
+
async for event in read_events(self, subject, options):
|
|
69
|
+
yield event
|
|
70
|
+
|
|
71
|
+
async def read_event_types(self) -> AsyncGenerator[EventType, None]:
|
|
72
|
+
async for event_type in read_event_types(self):
|
|
73
|
+
yield event_type
|
|
74
|
+
|
|
75
|
+
async def register_event_schema(self, event_type: str, json_schema: str) -> None:
|
|
76
|
+
await register_event_schema(self, event_type, json_schema)
|
|
77
|
+
|
|
78
|
+
async def observe_events(
|
|
79
|
+
self,
|
|
80
|
+
subject: str,
|
|
81
|
+
options: ObserveEventsOptions
|
|
82
|
+
) -> AsyncGenerator[StoreItem, None]:
|
|
83
|
+
async for event in observe_events(self, subject, options):
|
|
84
|
+
yield event
|
|
85
|
+
|
|
86
|
+
async def write_events(
|
|
87
|
+
self,
|
|
88
|
+
event_candidates: list[EventCandidate],
|
|
89
|
+
preconditions: list[Precondition] = None
|
|
90
|
+
) -> list[EventContext]:
|
|
91
|
+
if preconditions is None:
|
|
92
|
+
preconditions = []
|
|
93
|
+
return await write_events(self, event_candidates, preconditions)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import TypeVar
|
|
3
|
+
|
|
4
|
+
from ..errors.validation_error import ValidationError
|
|
5
|
+
from .event_context import EventContext
|
|
6
|
+
|
|
7
|
+
Self = TypeVar("Self", bound="Event")
|
|
8
|
+
|
|
9
|
+
# pylint: disable=R0917
|
|
10
|
+
# Reason: This class is expected to have many parameters
|
|
11
|
+
# due to its business context. Splitting it into smaller
|
|
12
|
+
# methods would increase cognitive load and make the
|
|
13
|
+
# code less readable.
|
|
14
|
+
class Event(EventContext):
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
data: dict,
|
|
18
|
+
source: str,
|
|
19
|
+
subject: str,
|
|
20
|
+
event_type: str,
|
|
21
|
+
spec_version: str,
|
|
22
|
+
event_id: str,
|
|
23
|
+
time: datetime,
|
|
24
|
+
data_content_type: str,
|
|
25
|
+
predecessor_hash: str,
|
|
26
|
+
trace_parent: str = None,
|
|
27
|
+
trace_state: str = None
|
|
28
|
+
):
|
|
29
|
+
super().__init__(
|
|
30
|
+
source,
|
|
31
|
+
subject,
|
|
32
|
+
event_type,
|
|
33
|
+
spec_version,
|
|
34
|
+
event_id,
|
|
35
|
+
time,
|
|
36
|
+
data_content_type,
|
|
37
|
+
predecessor_hash,
|
|
38
|
+
trace_parent,
|
|
39
|
+
trace_state
|
|
40
|
+
)
|
|
41
|
+
self.data = data
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def parse(unknown_object: dict) -> Self:
|
|
45
|
+
event_context = super(Event, Event).parse(unknown_object)
|
|
46
|
+
|
|
47
|
+
data = unknown_object.get('data')
|
|
48
|
+
if not isinstance(data, dict):
|
|
49
|
+
raise ValidationError(
|
|
50
|
+
f'Failed to parse data \'{data}\' to object.'
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
return Event(
|
|
54
|
+
data,
|
|
55
|
+
event_context.source,
|
|
56
|
+
event_context.subject,
|
|
57
|
+
event_context.type,
|
|
58
|
+
event_context.spec_version,
|
|
59
|
+
event_context.event_id,
|
|
60
|
+
event_context.time,
|
|
61
|
+
event_context.data_content_type,
|
|
62
|
+
event_context.predecessor_hash,
|
|
63
|
+
event_context.trace_parent,
|
|
64
|
+
event_context.trace_state
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def to_json(self):
|
|
68
|
+
json = super().to_json()
|
|
69
|
+
json['data'] = self.data
|
|
70
|
+
|
|
71
|
+
return json
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from .validate_subject import validate_subject
|
|
4
|
+
from .validate_type import validate_type
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class EventCandidate:
|
|
9
|
+
source: str
|
|
10
|
+
subject: str
|
|
11
|
+
type: str
|
|
12
|
+
data: dict
|
|
13
|
+
trace_parent: str = None
|
|
14
|
+
trace_state: str = None
|
|
15
|
+
|
|
16
|
+
def validate(self) -> None:
|
|
17
|
+
validate_subject(self.subject)
|
|
18
|
+
validate_type(self.type)
|
|
19
|
+
|
|
20
|
+
def to_json(self):
|
|
21
|
+
json = {
|
|
22
|
+
'data': self.data,
|
|
23
|
+
'source': self.source,
|
|
24
|
+
'subject': self.subject,
|
|
25
|
+
'type': self.type
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if self.trace_parent is not None:
|
|
29
|
+
json['traceparent'] = self.trace_parent
|
|
30
|
+
if self.trace_state is not None:
|
|
31
|
+
json['tracestate'] = self.trace_state
|
|
32
|
+
|
|
33
|
+
return json
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import TypeVar
|
|
4
|
+
|
|
5
|
+
from ..errors.internal_error import InternalError
|
|
6
|
+
from ..errors.validation_error import ValidationError
|
|
7
|
+
from .validate_subject import validate_subject
|
|
8
|
+
from .validate_type import validate_type
|
|
9
|
+
|
|
10
|
+
Self = TypeVar("Self", bound="EventContext")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class EventContext:
|
|
15
|
+
source: str
|
|
16
|
+
subject: str
|
|
17
|
+
type: str
|
|
18
|
+
spec_version: str
|
|
19
|
+
event_id: str
|
|
20
|
+
time: datetime
|
|
21
|
+
data_content_type: str
|
|
22
|
+
predecessor_hash: str
|
|
23
|
+
trace_parent: str = None
|
|
24
|
+
trace_state: str = None
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def parse(unknown_object: dict) -> Self:
|
|
28
|
+
source = unknown_object.get('source')
|
|
29
|
+
if not isinstance(source, str):
|
|
30
|
+
raise ValidationError(
|
|
31
|
+
f'Failed to parse source \'{source}\' to string.')
|
|
32
|
+
|
|
33
|
+
subject = unknown_object.get('subject')
|
|
34
|
+
if not isinstance(subject, str):
|
|
35
|
+
raise ValidationError(
|
|
36
|
+
f'Failed to parse subject \'{subject}\' to string.')
|
|
37
|
+
validate_subject(subject)
|
|
38
|
+
|
|
39
|
+
event_type = unknown_object.get('type')
|
|
40
|
+
if not isinstance(event_type, str):
|
|
41
|
+
raise ValidationError(
|
|
42
|
+
f'Failed to parse event_type \'{event_type}\' to string.')
|
|
43
|
+
validate_type(event_type)
|
|
44
|
+
|
|
45
|
+
spec_version = unknown_object.get('specversion')
|
|
46
|
+
if not isinstance(spec_version, str):
|
|
47
|
+
raise ValidationError(
|
|
48
|
+
f'Failed to parse spec_version \'{spec_version}\' to string.')
|
|
49
|
+
|
|
50
|
+
event_id = unknown_object.get('id')
|
|
51
|
+
if not isinstance(event_id, str):
|
|
52
|
+
raise ValidationError(
|
|
53
|
+
f'Failed to parse event_id \'{event_id}\' to string.')
|
|
54
|
+
|
|
55
|
+
time = unknown_object.get('time')
|
|
56
|
+
if not isinstance(time, str):
|
|
57
|
+
raise ValidationError(
|
|
58
|
+
f'Failed to parse time \'{time}\' to datetime.')
|
|
59
|
+
|
|
60
|
+
rest, sub_seconds = time.split('.')
|
|
61
|
+
sub_seconds = f'{sub_seconds[:6]:06}'
|
|
62
|
+
try:
|
|
63
|
+
time = datetime.fromisoformat(f'{rest}.{sub_seconds}')
|
|
64
|
+
except ValueError as value_error:
|
|
65
|
+
raise ValidationError(
|
|
66
|
+
f'Failed to parse time \'{time}\' to datetime.') from value_error
|
|
67
|
+
except Exception as other_error:
|
|
68
|
+
raise InternalError(str(other_error)) from other_error
|
|
69
|
+
|
|
70
|
+
data_content_type = unknown_object.get('datacontenttype')
|
|
71
|
+
if not isinstance(data_content_type, str):
|
|
72
|
+
raise ValidationError(
|
|
73
|
+
f'Failed to parse data_content_type \'{data_content_type}\' to string.')
|
|
74
|
+
|
|
75
|
+
predecessor_hash = unknown_object.get('predecessorhash')
|
|
76
|
+
if not isinstance(predecessor_hash, str):
|
|
77
|
+
raise ValidationError(
|
|
78
|
+
f'Failed to parse predecessor_hash \'{predecessor_hash}\' to string.')
|
|
79
|
+
|
|
80
|
+
trace_parent = unknown_object.get('traceparent')
|
|
81
|
+
if trace_parent is not None and not isinstance(trace_parent, str):
|
|
82
|
+
raise ValidationError(
|
|
83
|
+
f'Failed to parse trace_parent \'{trace_parent}\' to string.')
|
|
84
|
+
|
|
85
|
+
trace_state = unknown_object.get('tracestate')
|
|
86
|
+
if trace_state is not None and not isinstance(trace_state, str):
|
|
87
|
+
raise ValidationError(
|
|
88
|
+
f'Failed to parse trace_state \'{trace_state}\' to string.')
|
|
89
|
+
|
|
90
|
+
return EventContext(
|
|
91
|
+
source=source,
|
|
92
|
+
subject=subject,
|
|
93
|
+
type=event_type,
|
|
94
|
+
spec_version=spec_version,
|
|
95
|
+
event_id=event_id,
|
|
96
|
+
time=time,
|
|
97
|
+
data_content_type=data_content_type,
|
|
98
|
+
predecessor_hash=predecessor_hash,
|
|
99
|
+
trace_parent=trace_parent,
|
|
100
|
+
trace_state=trace_state,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def to_json(self):
|
|
104
|
+
json = {
|
|
105
|
+
'specversion': self.spec_version,
|
|
106
|
+
'id': self.event_id,
|
|
107
|
+
'time': self.time.isoformat(sep='T'),
|
|
108
|
+
'source': self.source,
|
|
109
|
+
'subject': self.subject,
|
|
110
|
+
'type': self.type,
|
|
111
|
+
'datacontenttype': self.data_content_type,
|
|
112
|
+
'predecessorhash': self.predecessor_hash
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if self.trace_parent is not None:
|
|
116
|
+
json['traceparent'] = self.trace_parent
|
|
117
|
+
if self.trace_state is not None:
|
|
118
|
+
json['tracestate'] = self.trace_state
|
|
119
|
+
|
|
120
|
+
return json
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from ..event.event_candidate import EventCandidate
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# pylint: disable=R0917
|
|
7
|
+
# Reason: This class is expected to have many parameters
|
|
8
|
+
# due to its business context. Splitting it into smaller
|
|
9
|
+
# methods would increase cognitive load and make the
|
|
10
|
+
# code less readable.
|
|
11
|
+
@dataclass
|
|
12
|
+
class Source:
|
|
13
|
+
source: str
|
|
14
|
+
|
|
15
|
+
def new_event(
|
|
16
|
+
self,
|
|
17
|
+
subject: str,
|
|
18
|
+
event_type: str,
|
|
19
|
+
data: dict,
|
|
20
|
+
trace_parent: str = None,
|
|
21
|
+
trace_state: str = None
|
|
22
|
+
) -> EventCandidate:
|
|
23
|
+
return EventCandidate(
|
|
24
|
+
self.source,
|
|
25
|
+
subject,
|
|
26
|
+
event_type,
|
|
27
|
+
data,
|
|
28
|
+
trace_parent,
|
|
29
|
+
trace_state
|
|
30
|
+
)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from ..errors.validation_error import ValidationError
|
|
4
|
+
|
|
5
|
+
WORD_PATTERN = '[0-9A-Za-z_-]+'
|
|
6
|
+
SUBJECT_PATTERN = f'^/({WORD_PATTERN}/)*({WORD_PATTERN}/?)?$'
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def validate_subject(subject: str) -> None:
|
|
10
|
+
if re.search(SUBJECT_PATTERN, subject) is None:
|
|
11
|
+
raise ValidationError(
|
|
12
|
+
f'Failed to validate subject: \'{subject}\' must be an absolute, slash-separated path.'
|
|
13
|
+
)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from ..errors.validation_error import ValidationError
|
|
4
|
+
|
|
5
|
+
TYPE_PATTERN = r'^[0-9A-Za-z_-]{2,}\.(?:[0-9A-Za-z_-]+\.)+[0-9A-Za-z_-]+$'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def validate_type(event_type: str) -> None:
|
|
9
|
+
if re.search(TYPE_PATTERN, event_type) is None:
|
|
10
|
+
raise ValidationError(
|
|
11
|
+
f'Failed to validate type: \'{event_type}\' must be a reverse domain name.'
|
|
12
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
ITEM_TYPE = 'item'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def is_item(message: Any) -> bool:
|
|
7
|
+
if not isinstance(message, dict) or message.get('type') != ITEM_TYPE:
|
|
8
|
+
return False
|
|
9
|
+
|
|
10
|
+
payload = message.get('payload')
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
isinstance(payload, dict)
|
|
14
|
+
and isinstance(payload.get('event'), dict)
|
|
15
|
+
and isinstance(payload.get('hash'), str)
|
|
16
|
+
)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
ERROR_TYPE = 'error'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def is_stream_error(message: Any) -> bool:
|
|
7
|
+
if not isinstance(message, dict) or message.get('type') != ERROR_TYPE:
|
|
8
|
+
return False
|
|
9
|
+
|
|
10
|
+
payload = message.get('payload')
|
|
11
|
+
|
|
12
|
+
return isinstance(payload, dict) and isinstance(payload.get('error'), str)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from collections.abc import AsyncGenerator
|
|
3
|
+
from http import HTTPStatus
|
|
4
|
+
|
|
5
|
+
from ..is_heartbeat import is_heartbeat
|
|
6
|
+
from ..is_item import is_item
|
|
7
|
+
from ..is_stream_error import is_stream_error
|
|
8
|
+
from ..parse_raw_message import parse_raw_message
|
|
9
|
+
from ...abstract_base_client import AbstractBaseClient
|
|
10
|
+
from ...errors.custom_error import CustomError
|
|
11
|
+
from ...errors.internal_error import InternalError
|
|
12
|
+
from ...errors.invalid_parameter_error import InvalidParameterError
|
|
13
|
+
from ...errors.server_error import ServerError
|
|
14
|
+
from ...errors.validation_error import ValidationError
|
|
15
|
+
from ...event.event import Event
|
|
16
|
+
from ...event.validate_subject import validate_subject
|
|
17
|
+
from ..store_item import StoreItem
|
|
18
|
+
from .observe_events_options import ObserveEventsOptions
|
|
19
|
+
from ...http_client.response import Response
|
|
20
|
+
|
|
21
|
+
# pylint: disable=R6007
|
|
22
|
+
# Reason: This method explicitly specifies the return type as None
|
|
23
|
+
# for better readability. Even though it is not necessary,
|
|
24
|
+
# it makes the return type clear without needing to read any
|
|
25
|
+
# documentation or code.
|
|
26
|
+
async def observe_events(
|
|
27
|
+
client: AbstractBaseClient,
|
|
28
|
+
subject: str,
|
|
29
|
+
options: ObserveEventsOptions
|
|
30
|
+
) -> AsyncGenerator[StoreItem, None]:
|
|
31
|
+
try:
|
|
32
|
+
validate_subject(subject)
|
|
33
|
+
except ValidationError as validation_error:
|
|
34
|
+
raise InvalidParameterError('subject', str(validation_error)) from validation_error
|
|
35
|
+
except Exception as other_error:
|
|
36
|
+
raise InternalError(str(other_error)) from other_error
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
options.validate()
|
|
40
|
+
except ValidationError as validation_error:
|
|
41
|
+
raise InvalidParameterError('options', str(validation_error)) from validation_error
|
|
42
|
+
except Exception as other_error:
|
|
43
|
+
raise InternalError(str(other_error)) from other_error
|
|
44
|
+
|
|
45
|
+
request_body = json.dumps({
|
|
46
|
+
'subject': subject,
|
|
47
|
+
'options': options.to_json()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
response: Response
|
|
51
|
+
try:
|
|
52
|
+
response = await client.http_client.post(
|
|
53
|
+
path='/api/observe-events',
|
|
54
|
+
request_body=request_body,
|
|
55
|
+
)
|
|
56
|
+
except CustomError as custom_error:
|
|
57
|
+
raise custom_error
|
|
58
|
+
except Exception as other_error:
|
|
59
|
+
raise InternalError(str(other_error)) from other_error
|
|
60
|
+
|
|
61
|
+
with response:
|
|
62
|
+
if response.status_code != HTTPStatus.OK:
|
|
63
|
+
raise ServerError(
|
|
64
|
+
f'Unexpected response status: '
|
|
65
|
+
f'{response.status_code} {HTTPStatus(response.status_code).phrase}'
|
|
66
|
+
)
|
|
67
|
+
async for raw_message in response.body:
|
|
68
|
+
message = parse_raw_message(raw_message)
|
|
69
|
+
|
|
70
|
+
if is_heartbeat(message):
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
if is_stream_error(message):
|
|
74
|
+
raise ServerError(f'{message["payload"]["error"]}.')
|
|
75
|
+
|
|
76
|
+
if is_item(message):
|
|
77
|
+
event = Event.parse(message['payload']['event'])
|
|
78
|
+
|
|
79
|
+
yield StoreItem(event, message['payload']['hash'])
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
raise ServerError(
|
|
83
|
+
f'Failed to read events, an unexpected stream item was received: '
|
|
84
|
+
f'{message}.'
|
|
85
|
+
)
|