Skip to content

Commit bafbaee

Browse files
authored
Change all client types to be frozen datatypes (#214)
2 parents 463a6af + 32165ff commit bafbaee

File tree

6 files changed

+138
-179
lines changed

6 files changed

+138
-179
lines changed

RELEASE_NOTES.md

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,18 @@
22

33
## Summary
44

5-
This release adds enhanced filtering capabilities for gridpool orders and trades, with support for tag-based filtering and more flexible time-based queries.
5+
<!-- Here goes a general summary of what this release is about -->
66

7-
## New Features
8-
9-
* **Tag filtering for gridpool trades**: The `gridpool_trades()` method now accepts a `tag` parameter to filter trades by tag. The `GridpoolTradeFilter` dataclass has been updated accordingly.
7+
## Upgrading
108

11-
* **Flexible time filtering with `DeliveryTimeFilter`**: Replaced the restrictive `delivery_period` parameter with a more flexible `delivery_time_filter` across gridpool orders and trades methods. The new `DeliveryTimeFilter` supports:
12-
- Time interval filtering with optional start/end times
13-
- Multiple delivery duration filters
14-
- More granular control over time-based queries
9+
- `DeliveryPeriod` instances now require a `DeliveryDuration` as an argument in their constructors, and can't be instantiated from `timedelta`s any more. Instead, `DeliveryDuration`s can be created from `timedelta` instances with `DeliveryDuration.from_timedelta()`.
1510

16-
* **New types for time filtering**: Added `Interval` and `DeliveryTimeFilter` types to support the enhanced filtering API.
11+
- Timestamps are no-longer automatically converted to UTC. If provided timestamps are not in UTC, the client will raise an exception.
1712

18-
* Support tags in CLI create-order command.
19-
20-
## Breaking Changes
13+
## New Features
2114

22-
* The `delivery_period` parameter has been replaced with `delivery_time_filter` in the following methods:
23-
- `list_gridpool_orders()`
24-
- `stream_gridpool_orders()`
25-
- `gridpool_trades()`
15+
<!-- Here goes the main new features and examples or instructions on how to use them -->
2616

27-
Note: The `create_gridpool_order()` method maintains backward compatibility by keeping both parameters.
17+
## Bug Fixes
2818

29-
* Updated API imports from v1 to v1alpha8 for common types.
19+
<!-- Here goes notable bug fixes that are worth a special mention or explanation -->

src/frequenz/client/electricity_trading/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,9 +192,11 @@ async def stream_trades():
192192
DeliveryArea,
193193
DeliveryDuration,
194194
DeliveryPeriod,
195+
DeliveryTimeFilter,
195196
EnergyMarketCodeType,
196197
GridpoolOrderFilter,
197198
GridpoolTradeFilter,
199+
Interval,
198200
MarketActor,
199201
MarketSide,
200202
Order,
@@ -222,9 +224,11 @@ async def stream_trades():
222224
"DeliveryArea",
223225
"DeliveryDuration",
224226
"DeliveryPeriod",
227+
"DeliveryTimeFilter",
225228
"EnergyMarketCodeType",
226229
"GridpoolOrderFilter",
227230
"GridpoolTradeFilter",
231+
"Interval",
228232
"MarketSide",
229233
"MarketActor",
230234
"Order",

src/frequenz/client/electricity_trading/_types.py

Lines changed: 66 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,34 @@ class DeliveryDuration(enum.Enum):
312312
MINUTES_60 = delivery_duration_pb2.DeliveryDuration.DELIVERY_DURATION_60
313313
"""1-hour contract duration."""
314314

315+
@classmethod
316+
def from_timedelta(cls, duration: timedelta) -> Self:
317+
"""Convert a timedelta to a DeliveryDuration enum.
318+
319+
Args:
320+
duration: The duration as a timedelta.
321+
322+
Returns:
323+
The corresponding DeliveryDuration enum value.
324+
325+
Raises:
326+
ValueError: If the duration is not one of the supported values.
327+
"""
328+
minutes = duration.total_seconds() / 60
329+
match minutes:
330+
case 5:
331+
return DeliveryDuration.MINUTES_5
332+
case 15:
333+
return DeliveryDuration.MINUTES_15
334+
case 30:
335+
return DeliveryDuration.MINUTES_30
336+
case 60:
337+
return DeliveryDuration.MINUTES_60
338+
case _:
339+
raise ValueError(
340+
"Invalid duration value. Duration must be 5, 15, 30, or 60 minutes."
341+
)
342+
315343
@classmethod
316344
@from_pb
317345
def from_pb(
@@ -343,6 +371,7 @@ def to_pb(self) -> delivery_duration_pb2.DeliveryDuration.ValueType:
343371
return delivery_duration_pb2.DeliveryDuration.ValueType(self.value)
344372

345373

374+
@dataclass(frozen=True)
346375
class DeliveryPeriod:
347376
"""
348377
Time period during which the contract is delivered.
@@ -358,54 +387,10 @@ class DeliveryPeriod:
358387
duration: DeliveryDuration
359388
"""The length of the delivery period."""
360389

361-
def __init__(
362-
self,
363-
start: datetime,
364-
duration: timedelta,
365-
) -> None:
366-
"""
367-
Initialize the DeliveryPeriod object.
368-
369-
Args:
370-
start: Start UTC timestamp represents the beginning of the delivery period.
371-
duration: The length of the delivery period.
372-
373-
Raises:
374-
ValueError: If the start timestamp does not have a timezone.
375-
or if the duration is not 5, 15, 30, or 60 minutes.
376-
"""
377-
if start.tzinfo is None:
378-
raise ValueError("Start timestamp must have a timezone.")
379-
if start.tzinfo != timezone.utc:
380-
_logger.warning(
381-
"Start timestamp is not in UTC timezone. Converting to UTC."
382-
)
383-
start = start.astimezone(timezone.utc)
384-
self.start = start
385-
386-
minutes = duration.total_seconds() / 60
387-
match minutes:
388-
case 5:
389-
self.duration = DeliveryDuration.MINUTES_5
390-
case 15:
391-
self.duration = DeliveryDuration.MINUTES_15
392-
case 30:
393-
self.duration = DeliveryDuration.MINUTES_30
394-
case 60:
395-
self.duration = DeliveryDuration.MINUTES_60
396-
case _:
397-
raise ValueError(
398-
"Invalid duration value. Duration must be 5, 15, 30, or 60 minutes."
399-
)
400-
401-
def __hash__(self) -> int:
402-
"""
403-
Create hash of the DeliveryPeriod object.
404-
405-
Returns:
406-
Hash of the DeliveryPeriod object.
407-
"""
408-
return hash((self.start, self.duration))
390+
def __post_init__(self) -> None:
391+
"""Validate that the start timestamp is in UTC."""
392+
if self.start.tzinfo is None or self.start.tzinfo != timezone.utc:
393+
raise ValueError("Start timestamp must be in UTC timezone.")
409394

410395
def __eq__(
411396
self,
@@ -453,27 +438,10 @@ def from_pb(cls, delivery_period: delivery_duration_pb2.DeliveryPeriod) -> Self:
453438
454439
Returns:
455440
DeliveryPeriod object corresponding to the protobuf message.
456-
457-
Raises:
458-
ValueError: If the duration is not 5, 15, 30, or 60 minutes.
459441
"""
460442
start = delivery_period.start.ToDatetime(tzinfo=timezone.utc)
461443
delivery_duration_enum = DeliveryDuration.from_pb(delivery_period.duration)
462-
463-
match delivery_duration_enum:
464-
case DeliveryDuration.MINUTES_5:
465-
duration = timedelta(minutes=5)
466-
case DeliveryDuration.MINUTES_15:
467-
duration = timedelta(minutes=15)
468-
case DeliveryDuration.MINUTES_30:
469-
duration = timedelta(minutes=30)
470-
case DeliveryDuration.MINUTES_60:
471-
duration = timedelta(minutes=60)
472-
case _:
473-
raise ValueError(
474-
"Invalid duration value. Duration must be 5, 15, 30, or 60 minutes."
475-
)
476-
return cls(start=start, duration=duration)
444+
return cls(start=start, duration=delivery_duration_enum)
477445

478446
def to_pb(self) -> delivery_duration_pb2.DeliveryPeriod:
479447
"""Convert a DeliveryPeriod object to protobuf DeliveryPeriod.
@@ -1025,7 +993,7 @@ def to_pb(
1025993
return self.value
1026994

1027995

1028-
@dataclass()
996+
@dataclass(frozen=True)
1029997
class Order: # pylint: disable=too-many-instance-attributes
1030998
"""Represents an order in the electricity market."""
1031999

@@ -1074,11 +1042,11 @@ class Order: # pylint: disable=too-many-instance-attributes
10741042
def __post_init__(self) -> None:
10751043
"""Post initialization checks to ensure that all datetimes are UTC."""
10761044
if self.valid_until is not None:
1077-
if self.valid_until.tzinfo is None:
1045+
if (
1046+
self.valid_until.tzinfo is None
1047+
or self.valid_until.tzinfo != timezone.utc
1048+
):
10781049
raise ValueError("Valid until must be a UTC datetime.")
1079-
if self.valid_until.tzinfo != timezone.utc:
1080-
_logger.warning("Valid until is not a UTC datetime. Converting to UTC.")
1081-
self.valid_until = self.valid_until.astimezone(timezone.utc)
10821050

10831051
@classmethod
10841052
@from_pb
@@ -1213,7 +1181,7 @@ def __eq__(self, other: object) -> bool:
12131181
)
12141182

12151183

1216-
@dataclass()
1184+
@dataclass(frozen=True)
12171185
class Trade: # pylint: disable=too-many-instance-attributes
12181186
"""Represents a private trade in the electricity market."""
12191187

@@ -1247,11 +1215,11 @@ class Trade: # pylint: disable=too-many-instance-attributes
12471215

12481216
def __post_init__(self) -> None:
12491217
"""Post initialization checks to ensure that all datetimes are UTC."""
1250-
if self.execution_time.tzinfo is None:
1251-
raise ValueError("Execution time must have timezone information")
1252-
if self.execution_time.tzinfo != timezone.utc:
1253-
_logger.warning("Execution timenis not in UTC timezone. Converting to UTC.")
1254-
self.execution_time = self.execution_time.astimezone(timezone.utc)
1218+
if (
1219+
self.execution_time.tzinfo is None
1220+
or self.execution_time.tzinfo != timezone.utc
1221+
):
1222+
raise ValueError("Execution time must be a UTC datetime.")
12551223

12561224
@classmethod
12571225
@from_pb
@@ -1347,7 +1315,7 @@ def to_pb(self) -> electricity_trading_pb2.OrderDetail.StateDetail:
13471315
)
13481316

13491317

1350-
@dataclass()
1318+
@dataclass(frozen=True)
13511319
class OrderDetail:
13521320
"""
13531321
Represents an order with full details, including its ID, state, and associated UTC timestamps.
@@ -1378,19 +1346,14 @@ def __post_init__(self) -> None:
13781346
ValueError: If create_time or modification_time do not have timezone information.
13791347
13801348
"""
1381-
if self.create_time.tzinfo is None:
1382-
raise ValueError("Create time must have timezone information")
1383-
if self.create_time.tzinfo != timezone.utc:
1384-
_logger.warning("Create time is not in UTC timezone. Converting to UTC.")
1385-
self.create_time = self.create_time.astimezone(timezone.utc)
1349+
if self.create_time.tzinfo is None or self.create_time.tzinfo != timezone.utc:
1350+
raise ValueError("Create time must be a UTC datetime.")
13861351

1387-
if self.modification_time.tzinfo is None:
1388-
raise ValueError("Modification time must have timezone information")
1389-
if self.modification_time.tzinfo != timezone.utc:
1390-
_logger.warning(
1391-
"Modification time is not in UTC timezone. Converting to UTC."
1392-
)
1393-
self.modification_time = self.modification_time.astimezone(timezone.utc)
1352+
if (
1353+
self.modification_time.tzinfo is None
1354+
or self.modification_time.tzinfo != timezone.utc
1355+
):
1356+
raise ValueError("Modification time must be a UTC datetime.")
13941357

13951358
@classmethod
13961359
@from_pb
@@ -1439,7 +1402,7 @@ def to_pb(self) -> electricity_trading_pb2.OrderDetail:
14391402
)
14401403

14411404

1442-
@dataclass()
1405+
@dataclass(frozen=True)
14431406
class PublicTrade: # pylint: disable=too-many-instance-attributes
14441407
"""Represents a public order in the market."""
14451408

@@ -1469,11 +1432,11 @@ class PublicTrade: # pylint: disable=too-many-instance-attributes
14691432

14701433
def __post_init__(self) -> None:
14711434
"""Post initialization checks to ensure that all datetimes are UTC."""
1472-
if self.execution_time.tzinfo is None:
1473-
raise ValueError("Execution time must have timezone information")
1474-
if self.execution_time.tzinfo != timezone.utc:
1475-
_logger.warning("Execution time is not in UTC timezone. Converting to UTC.")
1476-
self.execution_time = self.execution_time.astimezone(timezone.utc)
1435+
if (
1436+
self.execution_time.tzinfo is None
1437+
or self.execution_time.tzinfo != timezone.utc
1438+
):
1439+
raise ValueError("Execution time must be a UTC datetime.")
14771440

14781441
@classmethod
14791442
@from_pb
@@ -1967,7 +1930,7 @@ def to_pb(self) -> electricity_trading_pb2.PublicTradeFilter:
19671930
)
19681931

19691932

1970-
@dataclass()
1933+
@dataclass(frozen=True)
19711934
class UpdateOrder: # pylint: disable=too-many-instance-attributes
19721935
"""
19731936
Represents the order properties that can be updated after an order has been placed.
@@ -2010,12 +1973,10 @@ class UpdateOrder: # pylint: disable=too-many-instance-attributes
20101973

20111974
def __post_init__(self) -> None:
20121975
"""Post initialization checks to ensure that all datetimes are UTC."""
2013-
if self.valid_until is not None:
2014-
if self.valid_until.tzinfo is None:
2015-
raise ValueError("Valid until must be a UTC datetime.")
2016-
if self.valid_until.tzinfo != timezone.utc:
2017-
_logger.warning("Valid until is not a UTC datetime. Converting to UTC.")
2018-
self.valid_until = self.valid_until.astimezone(timezone.utc)
1976+
if self.valid_until is not None and (
1977+
self.valid_until.tzinfo is None or self.valid_until.tzinfo != timezone.utc
1978+
):
1979+
raise ValueError("Valid until must be a UTC datetime.")
20191980

20201981
@classmethod
20211982
@from_pb
@@ -2109,7 +2070,7 @@ def to_pb(self) -> electricity_trading_pb2.UpdateGridpoolOrderRequest.UpdateOrde
21092070
)
21102071

21112072

2112-
@dataclass()
2073+
@dataclass(frozen=True)
21132074
class PublicOrder: # pylint: disable=too-many-instance-attributes
21142075
"""Represents a public order in the market."""
21152076

@@ -2143,18 +2104,6 @@ class PublicOrder: # pylint: disable=too-many-instance-attributes
21432104
update_time: datetime
21442105
"""UTC Timestamp of the last update to the order."""
21452106

2146-
def __post_init__(self) -> None:
2147-
"""Post initialization checks to ensure that all datetimes are UTC."""
2148-
if self.delivery_period.start.tzinfo is None:
2149-
raise ValueError("Delivery period start must have timezone information")
2150-
if self.delivery_period.start.tzinfo != timezone.utc:
2151-
_logger.warning(
2152-
"Delivery period start is not in UTC timezone. Converting to UTC."
2153-
)
2154-
self.delivery_period.start = self.delivery_period.start.astimezone(
2155-
timezone.utc
2156-
)
2157-
21582107
@classmethod
21592108
@from_pb
21602109
def from_pb(
@@ -2222,7 +2171,7 @@ def to_pb(self) -> electricity_trading_pb2.PublicOrderBookRecord:
22222171
)
22232172

22242173

2225-
@dataclass()
2174+
@dataclass(frozen=True)
22262175
class PublicOrderBookFilter:
22272176
"""Parameters for filtering the public orders in the market."""
22282177

0 commit comments

Comments
 (0)