Skip to content

Commit 8806584

Browse files
committed
feat: change valdiation issues with LowerBound and UpperBound changes to object
1 parent 3e3b6fb commit 8806584

File tree

7 files changed

+104
-28
lines changed

7 files changed

+104
-28
lines changed
Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
11
from dataclasses import dataclass
22

3+
from eventsourcingdb_client_python.errors.invalid_parameter_error import InvalidParameterError
4+
35
@dataclass
46
class LowerBound:
57
id: int
68
type: str
79

810
def __post_init__(self):
911
if self.type not in {"inclusive", "exclusive"}:
10-
raise ValueError("type must be either 'inclusive' or 'exclusive'")
12+
raise InvalidParameterError(
13+
parameter_name="LowerBound",
14+
reason="type must be either 'inclusive' or 'exclusive'"
15+
)
16+
17+
if self.id < 0:
18+
raise InvalidParameterError(
19+
parameter_name="LowerBound",
20+
reason="id must be non-negative"
21+
)

eventsourcingdb_client_python/handlers/observe_events/observe_events.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,16 @@ async def observe_events(
7575

7676
if is_event(message):
7777
event = Event.parse(message['payload'])
78+
# Add client-side filtering by event ID
79+
event_id = int(message['payload']['id'])
80+
81+
if options.lower_bound is not None:
82+
# For inclusive, include events with ID >= lower bound
83+
if options.lower_bound.type == 'inclusive' and event_id < options.lower_bound.id:
84+
continue
85+
# For exclusive, include events with ID > lower bound
86+
if options.lower_bound.type == 'exclusive' and event_id <= options.lower_bound.id:
87+
continue
7888

7989
yield StoreItem(event, message['payload']['hash'])
8090
continue

eventsourcingdb_client_python/handlers/observe_events/observe_events_options.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
from ..lower_bound import LowerBound
44
from ...errors.validation_error import ValidationError
5+
from ...event.validate_subject import validate_subject
6+
from ...event.validate_type import validate_type
7+
from ...util.is_non_negativ_integer import is_non_negative_integer
58
from .observe_from_latest_event import ObserveFromLatestEvent
69

7-
810
@dataclass
911
class ObserveEventsOptions:
1012
recursive: bool
@@ -18,8 +20,29 @@ def validate(self) -> None:
1820
'ObserveEventsOptions are invalid: lower_bound must be a LowerBound object.'
1921
)
2022

21-
# Rest of validation logic
22-
# ...
23+
if self.from_latest_event is not None:
24+
if self.lower_bound is not None:
25+
raise ValidationError(
26+
'ReadEventsOptions are invalid: '
27+
'lowerBoundId and fromLatestEvent are mutually exclusive'
28+
)
29+
30+
try:
31+
validate_subject(self.from_latest_event.subject)
32+
except ValidationError as validation_error:
33+
raise ValidationError(
34+
f'ReadEventsOptions are invalid: '
35+
f'Failed to validate \'from_latest_event\': {str(validation_error)}'
36+
) from validation_error
37+
38+
try:
39+
validate_type(self.from_latest_event.type)
40+
except ValidationError as validation_error:
41+
raise ValidationError(
42+
f'ReadEventsOptions are invalid: '
43+
f'Failed to validate \'from_latest_event\': {str(validation_error)}'
44+
) from validation_error
45+
2346

2447
def to_json(self):
2548
json = {

eventsourcingdb_client_python/handlers/read_events/read_events.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ async def read_events(
8989
# For exclusive, include events with ID < upper bound
9090
if options.upper_bound.type == 'exclusive' and event_id >= options.upper_bound.id:
9191
continue
92-
92+
9393
yield StoreItem(event, message['payload']['hash'])
9494
continue
95+
96+
raise ServerError(
97+
f'Failed to read events, an unexpected stream item was received: '
98+
f'{message}.'
99+
)

eventsourcingdb_client_python/handlers/read_events/read_events_options.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,20 @@ def validate(self) -> None:
3636
'lowerBound and fromLatestEvent are mutually exclusive'
3737
)
3838

39-
# Rest of validation remains the same
40-
# ...
39+
try:
40+
validate_subject(self.from_latest_event.subject)
41+
except ValidationError as validation_error:
42+
raise ValidationError(
43+
f'ReadEventsOptions are invalid: '
44+
f'from_latest_event.subject: {str(validation_error)}'
45+
) from validation_error
46+
47+
# Add validation for empty type too
48+
if not self.from_latest_event.type:
49+
raise ValidationError(
50+
'ReadEventsOptions are invalid: '
51+
'from_latest_event.type cannot be empty'
52+
)
4153

4254
def to_json(self):
4355
json = {
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from dataclasses import dataclass
22

3+
from eventsourcingdb_client_python.errors.invalid_parameter_error import InvalidParameterError
4+
35

46
@dataclass
57
class UpperBound:
@@ -8,4 +10,7 @@ class UpperBound:
810

911
def __post_init__(self):
1012
if self.type not in {"inclusive", "exclusive"}:
11-
raise ValueError("type must be either 'inclusive' or 'exclusive'")
13+
raise InvalidParameterError(
14+
parameter_name="UpperBound",
15+
reason="type must be either 'inclusive' or 'exclusive'"
16+
)

tests/test_observe_events.py

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from eventsourcingdb_client_python.errors.client_error import ClientError
88
from eventsourcingdb_client_python.errors.invalid_parameter_error import InvalidParameterError
99
from eventsourcingdb_client_python.errors.server_error import ServerError
10+
from eventsourcingdb_client_python.handlers.lower_bound import LowerBound
1011
from eventsourcingdb_client_python.handlers.observe_events import \
1112
ObserveEventsOptions, \
1213
ObserveFromLatestEvent, \
@@ -263,7 +264,10 @@ async def test_observes_event_starting_from_given_lower_bound(
263264
'/users',
264265
ObserveEventsOptions(
265266
recursive=True,
266-
lower_bound='2'
267+
lower_bound=LowerBound(
268+
id=2,
269+
type='inclusive'
270+
)
267271
)
268272
):
269273
observed_items.append(event)
@@ -322,7 +326,10 @@ async def test_throws_error_for_mutually_exclusive_options(
322326
'/users',
323327
ObserveEventsOptions(
324328
recursive=True,
325-
lower_bound='3',
329+
lower_bound=LowerBound(
330+
id=3,
331+
type='excl'
332+
),
326333
from_latest_event=ObserveFromLatestEvent(
327334
subject='/',
328335
type='com.foo.bar',
@@ -331,23 +338,23 @@ async def test_throws_error_for_mutually_exclusive_options(
331338
)
332339
):
333340
pass
334-
335-
@staticmethod
336-
@pytest.mark.asyncio
337-
async def test_throws_error_for_non_integer_lower_bound(
338-
prepared_database: Database
339-
):
340-
client = prepared_database.with_authorization.client
341-
342-
with pytest.raises(InvalidParameterError):
343-
async for _ in client.observe_events(
344-
'/users',
345-
ObserveEventsOptions(
346-
recursive=True,
347-
lower_bound='hello',
348-
)
349-
):
350-
pass
341+
# TODO: Test restructured. String not used in the new code.
342+
# @staticmethod
343+
# @pytest.mark.asyncio
344+
# async def test_throws_error_for_non_integer_lower_bound(
345+
# prepared_database: Database
346+
# ):
347+
# client = prepared_database.with_authorization.client
348+
#
349+
# with pytest.raises(InvalidParameterError):
350+
# async for _ in client.observe_events(
351+
# '/users',
352+
# ObserveEventsOptions(
353+
# recursive=True,
354+
# lower_bound='hello',
355+
# )
356+
# ):
357+
# pass
351358

352359
@staticmethod
353360
@pytest.mark.asyncio
@@ -361,7 +368,10 @@ async def test_throws_error_for_negative_lower_bound(
361368
'/users',
362369
ObserveEventsOptions(
363370
recursive=True,
364-
lower_bound='-1',
371+
lower_bound=LowerBound(
372+
id=-1,
373+
type='inclusive'
374+
),
365375
)
366376
):
367377
pass

0 commit comments

Comments
 (0)