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.
Files changed (64) hide show
  1. eventsourcingdb/__init__.py +2 -0
  2. eventsourcingdb/abstract_base_client.py +9 -0
  3. eventsourcingdb/client.py +93 -0
  4. eventsourcingdb/client_configuration.py +10 -0
  5. eventsourcingdb/client_options.py +8 -0
  6. eventsourcingdb/errors/__init__.py +0 -0
  7. eventsourcingdb/errors/client_error.py +6 -0
  8. eventsourcingdb/errors/custom_error.py +2 -0
  9. eventsourcingdb/errors/internal_error.py +6 -0
  10. eventsourcingdb/errors/invalid_parameter_error.py +7 -0
  11. eventsourcingdb/errors/server_error.py +6 -0
  12. eventsourcingdb/errors/validation_error.py +6 -0
  13. eventsourcingdb/event/__init__.py +0 -0
  14. eventsourcingdb/event/event.py +71 -0
  15. eventsourcingdb/event/event_candidate.py +33 -0
  16. eventsourcingdb/event/event_context.py +120 -0
  17. eventsourcingdb/event/source.py +30 -0
  18. eventsourcingdb/event/validate_subject.py +13 -0
  19. eventsourcingdb/event/validate_type.py +12 -0
  20. eventsourcingdb/handlers/__init__.py +0 -0
  21. eventsourcingdb/handlers/is_heartbeat.py +7 -0
  22. eventsourcingdb/handlers/is_item.py +16 -0
  23. eventsourcingdb/handlers/is_stream_error.py +12 -0
  24. eventsourcingdb/handlers/observe_events/__init__.py +4 -0
  25. eventsourcingdb/handlers/observe_events/if_event_is_missing_during_observe.py +6 -0
  26. eventsourcingdb/handlers/observe_events/observe_events.py +85 -0
  27. eventsourcingdb/handlers/observe_events/observe_events_options.py +55 -0
  28. eventsourcingdb/handlers/observe_events/observe_from_latest_event.py +17 -0
  29. eventsourcingdb/handlers/parse_raw_message.py +17 -0
  30. eventsourcingdb/handlers/ping.py +12 -0
  31. eventsourcingdb/handlers/read_event_types/__init__.py +0 -0
  32. eventsourcingdb/handlers/read_event_types/event_type.py +42 -0
  33. eventsourcingdb/handlers/read_event_types/is_event_type.py +12 -0
  34. eventsourcingdb/handlers/read_event_types/read_event_types.py +60 -0
  35. eventsourcingdb/handlers/read_events/__init__.py +4 -0
  36. eventsourcingdb/handlers/read_events/if_event_is_missing_during_read.py +6 -0
  37. eventsourcingdb/handlers/read_events/order.py +6 -0
  38. eventsourcingdb/handlers/read_events/read_events.py +81 -0
  39. eventsourcingdb/handlers/read_events/read_events_options.py +66 -0
  40. eventsourcingdb/handlers/read_events/read_from_latest_event.py +17 -0
  41. eventsourcingdb/handlers/read_subjects/__init__.py +1 -0
  42. eventsourcingdb/handlers/read_subjects/is_subject.py +12 -0
  43. eventsourcingdb/handlers/read_subjects/read_subjects.py +66 -0
  44. eventsourcingdb/handlers/register_event_schema/__init__.py +0 -0
  45. eventsourcingdb/handlers/register_event_schema/register_event_schema.py +51 -0
  46. eventsourcingdb/handlers/store_item.py +8 -0
  47. eventsourcingdb/handlers/write_events/__init__.py +4 -0
  48. eventsourcingdb/handlers/write_events/preconditions.py +36 -0
  49. eventsourcingdb/handlers/write_events/write_events.py +81 -0
  50. eventsourcingdb/http_client/__init__.py +0 -0
  51. eventsourcingdb/http_client/http_client.py +189 -0
  52. eventsourcingdb/http_client/response.py +33 -0
  53. eventsourcingdb/util/__init__.py +0 -0
  54. eventsourcingdb/util/get_randomized_duration.py +10 -0
  55. eventsourcingdb/util/is_non_negativ_integer.py +8 -0
  56. eventsourcingdb/util/retry/__init__.py +0 -0
  57. eventsourcingdb/util/retry/retry_error.py +16 -0
  58. eventsourcingdb/util/retry/retry_result.py +17 -0
  59. eventsourcingdb/util/retry/retry_with_backoff.py +34 -0
  60. eventsourcingdb/util/url.py +5 -0
  61. eventsourcingdb-0.8.2.dist-info/LICENSE.md +21 -0
  62. eventsourcingdb-0.8.2.dist-info/METADATA +18 -0
  63. eventsourcingdb-0.8.2.dist-info/RECORD +64 -0
  64. eventsourcingdb-0.8.2.dist-info/WHEEL +4 -0
@@ -0,0 +1,2 @@
1
+ from .client import Client as EsdbClient
2
+ from .client_options import ClientOptions as EsdbClientOptions
@@ -0,0 +1,9 @@
1
+ from abc import ABC, abstractmethod
2
+ from .http_client.http_client import HttpClient
3
+
4
+
5
+ class AbstractBaseClient(ABC):
6
+ @property
7
+ @abstractmethod
8
+ def http_client(self) -> HttpClient:
9
+ raise NotImplementedError()
@@ -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)
@@ -0,0 +1,10 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class ClientConfiguration:
6
+ base_url: str
7
+ timeout_seconds: int
8
+ access_token: str
9
+ protocol_version: str
10
+ max_tries: int
@@ -0,0 +1,8 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class ClientOptions:
6
+ timeout_seconds: int = 10
7
+ protocol_version: str = '1.0.0'
8
+ max_tries: int = 10
File without changes
@@ -0,0 +1,6 @@
1
+ from .custom_error import CustomError
2
+
3
+
4
+ class ClientError(CustomError):
5
+ def __init__(self, cause: str):
6
+ super().__init__(f'Client error occurred: {cause}')
@@ -0,0 +1,2 @@
1
+ class CustomError(Exception):
2
+ pass
@@ -0,0 +1,6 @@
1
+ from .custom_error import CustomError
2
+
3
+
4
+ class InternalError(CustomError):
5
+ def __init__(self, cause: str):
6
+ super().__init__(f'Internal error occurred: {cause}')
@@ -0,0 +1,7 @@
1
+ from .custom_error import CustomError
2
+
3
+
4
+ class InvalidParameterError(CustomError):
5
+ def __init__(self, parameter_name: str, reason: str):
6
+ super().__init__(
7
+ f'Parameter \'{parameter_name}\' is invalid: {reason}')
@@ -0,0 +1,6 @@
1
+ from .custom_error import CustomError
2
+
3
+
4
+ class ServerError(CustomError):
5
+ def __init__(self, cause: str):
6
+ super().__init__(f'Server error occurred: {cause}')
@@ -0,0 +1,6 @@
1
+ from .custom_error import CustomError
2
+
3
+
4
+ class ValidationError(CustomError):
5
+ def __init__(self, cause: str):
6
+ super().__init__(f'Validation error occurred: {cause}')
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,7 @@
1
+ from typing import Any
2
+
3
+ HEARTBEAT_TYPE = 'heartbeat'
4
+
5
+
6
+ def is_heartbeat(message: Any) -> bool:
7
+ return isinstance(message, dict) and message.get('type') == HEARTBEAT_TYPE
@@ -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,4 @@
1
+ from .observe_events import observe_events
2
+ from .observe_events_options import ObserveEventsOptions
3
+ from .observe_from_latest_event import ObserveFromLatestEvent
4
+ from .if_event_is_missing_during_observe import IfEventIsMissingDuringObserve
@@ -0,0 +1,6 @@
1
+ from enum import Enum
2
+
3
+
4
+ class IfEventIsMissingDuringObserve(str, Enum):
5
+ WAIT_FOR_EVENT = 'wait-for-event'
6
+ READ_EVERYTHING = 'read-everything'
@@ -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
+ }