eventsourcingdb 0.8.2__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.
- eventsourcingdb/__init__.py +2 -0
- eventsourcingdb/abstract_base_client.py +9 -0
- eventsourcingdb/client.py +93 -0
- eventsourcingdb/client_configuration.py +10 -0
- eventsourcingdb/client_options.py +8 -0
- eventsourcingdb/errors/__init__.py +0 -0
- eventsourcingdb/errors/client_error.py +6 -0
- eventsourcingdb/errors/custom_error.py +2 -0
- eventsourcingdb/errors/internal_error.py +6 -0
- eventsourcingdb/errors/invalid_parameter_error.py +7 -0
- eventsourcingdb/errors/server_error.py +6 -0
- eventsourcingdb/errors/validation_error.py +6 -0
- eventsourcingdb/event/__init__.py +0 -0
- eventsourcingdb/event/event.py +71 -0
- eventsourcingdb/event/event_candidate.py +33 -0
- eventsourcingdb/event/event_context.py +120 -0
- eventsourcingdb/event/source.py +30 -0
- eventsourcingdb/event/validate_subject.py +13 -0
- eventsourcingdb/event/validate_type.py +12 -0
- eventsourcingdb/handlers/__init__.py +0 -0
- eventsourcingdb/handlers/is_heartbeat.py +7 -0
- eventsourcingdb/handlers/is_item.py +16 -0
- eventsourcingdb/handlers/is_stream_error.py +12 -0
- eventsourcingdb/handlers/observe_events/__init__.py +4 -0
- eventsourcingdb/handlers/observe_events/if_event_is_missing_during_observe.py +6 -0
- eventsourcingdb/handlers/observe_events/observe_events.py +85 -0
- eventsourcingdb/handlers/observe_events/observe_events_options.py +55 -0
- eventsourcingdb/handlers/observe_events/observe_from_latest_event.py +17 -0
- eventsourcingdb/handlers/parse_raw_message.py +17 -0
- eventsourcingdb/handlers/ping.py +12 -0
- eventsourcingdb/handlers/read_event_types/__init__.py +0 -0
- eventsourcingdb/handlers/read_event_types/event_type.py +42 -0
- eventsourcingdb/handlers/read_event_types/is_event_type.py +12 -0
- eventsourcingdb/handlers/read_event_types/read_event_types.py +60 -0
- eventsourcingdb/handlers/read_events/__init__.py +4 -0
- eventsourcingdb/handlers/read_events/if_event_is_missing_during_read.py +6 -0
- eventsourcingdb/handlers/read_events/order.py +6 -0
- eventsourcingdb/handlers/read_events/read_events.py +81 -0
- eventsourcingdb/handlers/read_events/read_events_options.py +66 -0
- eventsourcingdb/handlers/read_events/read_from_latest_event.py +17 -0
- eventsourcingdb/handlers/read_subjects/__init__.py +1 -0
- eventsourcingdb/handlers/read_subjects/is_subject.py +12 -0
- eventsourcingdb/handlers/read_subjects/read_subjects.py +66 -0
- eventsourcingdb/handlers/register_event_schema/__init__.py +0 -0
- eventsourcingdb/handlers/register_event_schema/register_event_schema.py +51 -0
- eventsourcingdb/handlers/store_item.py +8 -0
- eventsourcingdb/handlers/write_events/__init__.py +4 -0
- eventsourcingdb/handlers/write_events/preconditions.py +36 -0
- eventsourcingdb/handlers/write_events/write_events.py +81 -0
- eventsourcingdb/http_client/__init__.py +0 -0
- eventsourcingdb/http_client/http_client.py +189 -0
- eventsourcingdb/http_client/response.py +33 -0
- eventsourcingdb/util/__init__.py +0 -0
- eventsourcingdb/util/get_randomized_duration.py +10 -0
- eventsourcingdb/util/is_non_negativ_integer.py +8 -0
- eventsourcingdb/util/retry/__init__.py +0 -0
- eventsourcingdb/util/retry/retry_error.py +16 -0
- eventsourcingdb/util/retry/retry_result.py +17 -0
- eventsourcingdb/util/retry/retry_with_backoff.py +34 -0
- eventsourcingdb/util/url.py +5 -0
- eventsourcingdb-0.8.2.dist-info/LICENSE.md +21 -0
- eventsourcingdb-0.8.2.dist-info/METADATA +18 -0
- eventsourcingdb-0.8.2.dist-info/RECORD +64 -0
- eventsourcingdb-0.8.2.dist-info/WHEEL +4 -0
|
@@ -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
|
+
)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from ...errors.validation_error import ValidationError
|
|
4
|
+
from ...event.validate_subject import validate_subject
|
|
5
|
+
from ...event.validate_type import validate_type
|
|
6
|
+
from ...util.is_non_negativ_integer import is_non_negative_integer
|
|
7
|
+
from .observe_from_latest_event import ObserveFromLatestEvent
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class ObserveEventsOptions:
|
|
12
|
+
recursive: bool
|
|
13
|
+
lower_bound_id: str | None = None
|
|
14
|
+
from_latest_event: ObserveFromLatestEvent | None = None
|
|
15
|
+
|
|
16
|
+
def validate(self) -> None:
|
|
17
|
+
if self.lower_bound_id is not None and not is_non_negative_integer(self.lower_bound_id):
|
|
18
|
+
raise ValidationError(
|
|
19
|
+
'ReadEventOptions are invalid: lower_bound_id must be 0 or greater.'
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
if self.from_latest_event is not None:
|
|
23
|
+
if self.lower_bound_id is not None:
|
|
24
|
+
raise ValidationError(
|
|
25
|
+
'ReadEventsOptions are invalid: '
|
|
26
|
+
'lowerBoundId and fromLatestEvent are mutually exclusive'
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
validate_subject(self.from_latest_event.subject)
|
|
31
|
+
except ValidationError as validation_error:
|
|
32
|
+
raise ValidationError(
|
|
33
|
+
f'ReadEventsOptions are invalid: '
|
|
34
|
+
f'Failed to validate \'from_latest_event\': {str(validation_error)}'
|
|
35
|
+
) from validation_error
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
validate_type(self.from_latest_event.type)
|
|
39
|
+
except ValidationError as validation_error:
|
|
40
|
+
raise ValidationError(
|
|
41
|
+
f'ReadEventsOptions are invalid: '
|
|
42
|
+
f'Failed to validate \'from_latest_event\': {str(validation_error)}'
|
|
43
|
+
) from validation_error
|
|
44
|
+
|
|
45
|
+
def to_json(self):
|
|
46
|
+
json = {
|
|
47
|
+
'recursive': self.recursive
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if self.lower_bound_id is not None:
|
|
51
|
+
json['lowerBoundId'] = self.lower_bound_id
|
|
52
|
+
if self.from_latest_event is not None:
|
|
53
|
+
json['fromLatestEvent'] = self.from_latest_event.to_json()
|
|
54
|
+
|
|
55
|
+
return json
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from .if_event_is_missing_during_observe import IfEventIsMissingDuringObserve
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class ObserveFromLatestEvent:
|
|
8
|
+
subject: str
|
|
9
|
+
type: str
|
|
10
|
+
if_event_is_missing: IfEventIsMissingDuringObserve
|
|
11
|
+
|
|
12
|
+
def to_json(self):
|
|
13
|
+
return {
|
|
14
|
+
'subject': self.subject,
|
|
15
|
+
'type': self.type,
|
|
16
|
+
'ifEventIsMissing': self.if_event_is_missing
|
|
17
|
+
}
|