Skip to content

Commit 6a62f87

Browse files
committed
Add client implementation
Signed-off-by: Mathias L. Baumann <[email protected]>
1 parent 1d5c6bb commit 6a62f87

File tree

5 files changed

+1240
-1
lines changed

5 files changed

+1240
-1
lines changed

src/frequenz/client/dispatch/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,7 @@
22
# Copyright © 2024 Frequenz Energy-as-a-Service GmbH
33

44
"""Dispatch API client for Python."""
5+
6+
from ._client import Client
7+
8+
__all__ = ["Client"]
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
# License: MIT
2+
# Copyright © 2024 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Dispatch API client for Python."""
5+
from datetime import datetime, timedelta
6+
from typing import Any, Awaitable
7+
8+
import grpc
9+
from frequenz.api.dispatch.v1 import dispatch_pb2_grpc
10+
11+
# pylint: disable=no-name-in-module
12+
from frequenz.api.dispatch.v1.dispatch_pb2 import (
13+
DispatchDeleteRequest,
14+
DispatchFilter,
15+
DispatchGetRequest,
16+
DispatchList,
17+
DispatchListRequest,
18+
DispatchUpdateRequest,
19+
)
20+
from frequenz.api.dispatch.v1.dispatch_pb2 import (
21+
TimeIntervalFilter as PBTimeIntervalFilter,
22+
)
23+
from google.protobuf.timestamp_pb2 import Timestamp
24+
25+
from ._types import (
26+
ComponentSelector,
27+
Dispatch,
28+
DispatchCreateRequest,
29+
RecurrenceRule,
30+
_component_selector_to_protobuf,
31+
)
32+
33+
# pylint: enable=no-name-in-module
34+
35+
36+
class Client:
37+
"""Dispatch API client."""
38+
39+
def __init__(self, grpc_channel: grpc.aio.Channel, svc_addr: str) -> None:
40+
"""Initialize the client.
41+
42+
Args:
43+
grpc_channel: gRPC channel to use for communication with the API.
44+
svc_addr: Address of the service to connect to.
45+
"""
46+
self._svc_addr = svc_addr
47+
self._stub = dispatch_pb2_grpc.MicrogridDispatchServiceStub(grpc_channel)
48+
49+
# pylint: disable=too-many-arguments, too-many-locals
50+
async def list(
51+
self,
52+
microgrid_id: int,
53+
component_selectors: list[ComponentSelector] | None = None,
54+
start_from: datetime | None = None,
55+
start_to: datetime | None = None,
56+
end_from: datetime | None = None,
57+
end_to: datetime | None = None,
58+
is_active: bool | None = None,
59+
is_dry_run: bool | None = None,
60+
) -> list[Dispatch]:
61+
"""List dispatches.
62+
63+
Args:
64+
microgrid_id: The microgrid_id to list dispatches for.
65+
component_selectors: optional, list of component ids or categories to filter by.
66+
start_from: optional, filter by start_time >= start_from.
67+
start_to: optional, filter by start_time < start_to.
68+
end_from: optional, filter by end_time >= end_from.
69+
end_to: optional, filter by end_time < end_to.
70+
is_active: optional, filter by is_active status.
71+
is_dry_run: optional, filter by is_dry_run status.
72+
73+
Returns:
74+
DispatchList: List of dispatches.
75+
"""
76+
time_interval = None
77+
78+
def to_timestamp(dt: datetime | None) -> Timestamp | None:
79+
if dt is None:
80+
return None
81+
82+
ts = Timestamp()
83+
ts.FromDatetime(dt)
84+
return ts
85+
86+
if start_from or start_to or end_from or end_to:
87+
time_interval = PBTimeIntervalFilter(
88+
start_from=to_timestamp(start_from),
89+
start_to=to_timestamp(start_to),
90+
end_from=to_timestamp(end_from),
91+
end_to=to_timestamp(end_to),
92+
)
93+
94+
selectors = []
95+
96+
for selector in component_selectors if component_selectors else []:
97+
selectors.append(_component_selector_to_protobuf(selector))
98+
99+
filters = DispatchFilter(
100+
selectors=selectors,
101+
time_interval=time_interval,
102+
is_active=is_active,
103+
is_dry_run=is_dry_run,
104+
)
105+
request = DispatchListRequest(microgrid_id=microgrid_id, filter=filters)
106+
107+
response: Awaitable[DispatchList]
108+
response = self._stub.ListMicrogridDispatches(request) # type: ignore
109+
return list(map(Dispatch.from_protobuf, (await response).dispatches))
110+
111+
async def create(
112+
self,
113+
microgrid_id: int,
114+
_type: str,
115+
start_time: datetime,
116+
duration: timedelta,
117+
selector: ComponentSelector,
118+
is_active: bool,
119+
is_dry_run: bool,
120+
payload: dict[str, Any],
121+
recurrence: RecurrenceRule,
122+
) -> None:
123+
"""Create a dispatch.
124+
125+
Args:
126+
microgrid_id: The microgrid_id to create the dispatch for.
127+
_type: User defined string to identify the dispatch type.
128+
start_time: The start time of the dispatch.
129+
duration: The duration of the dispatch.
130+
selector: The component selector for the dispatch.
131+
is_active: The is_active status of the dispatch.
132+
is_dry_run: The is_dry_run status of the dispatch.
133+
payload: The payload of the dispatch.
134+
recurrence: The recurrence rule of the dispatch.
135+
"""
136+
request = DispatchCreateRequest(
137+
microgrid_id=microgrid_id,
138+
type=_type,
139+
start_time=start_time,
140+
duration=duration,
141+
selector=selector,
142+
is_active=is_active,
143+
is_dry_run=is_dry_run,
144+
payload=payload,
145+
recurrence=recurrence,
146+
).to_protobuf()
147+
148+
await self._stub.CreateMicrogridDispatch(request) # type: ignore
149+
150+
async def update(
151+
self,
152+
dispatch_id: int,
153+
new_fields: dict[str, Any],
154+
) -> None:
155+
"""Update a dispatch.
156+
157+
The `new_fields` argument is a dictionary of fields to update. The keys are
158+
the field names, and the values are the new values for the fields.
159+
160+
For recurrence fields, the keys are preceeded by "recurrence.".
161+
162+
Args:
163+
dispatch_id: The dispatch_id to update.
164+
new_fields: The fields to update.
165+
"""
166+
msg = DispatchUpdateRequest(id=dispatch_id)
167+
168+
for key, val in new_fields.items():
169+
path = key.split(".")
170+
171+
match path[0]:
172+
case "type":
173+
msg.update.type = val
174+
case "start_time":
175+
msg.update.start_time.FromDatetime(val)
176+
case "duration":
177+
msg.update.duration = int(val.total_seconds())
178+
case "selector":
179+
msg.update.selector.CopyFrom(_component_selector_to_protobuf(val))
180+
case "is_active":
181+
msg.update.is_active = val
182+
case "is_dry_run":
183+
msg.update.is_dry_run = val
184+
case "recurrence":
185+
match path[1]:
186+
case "freq":
187+
msg.update.recurrence.freq = val
188+
# Proto uses "freq" instead of "frequency"
189+
case "frequency":
190+
msg.update.recurrence.freq = val
191+
# Correct the key to "recurrence.freq"
192+
key = "recurrence.freq"
193+
case "interval":
194+
msg.update.recurrence.interval = val
195+
case "end_criteria":
196+
msg.update.recurrence.end_criteria.CopyFrom(
197+
val.to_protobuf()
198+
)
199+
case "byminutes":
200+
msg.update.recurrence.byminutes.extend(val)
201+
case "byhours":
202+
msg.update.recurrence.byhours.extend(val)
203+
case "byweekdays":
204+
msg.update.recurrence.byweekdays.extend(val)
205+
case "bymonthdays":
206+
msg.update.recurrence.bymonthdays.extend(val)
207+
case "bymonths":
208+
msg.update.recurrence.bymonths.extend(val)
209+
210+
msg.update_mask.paths.append(key)
211+
212+
await self._stub.UpdateMicrogridDispatch(msg) # type: ignore
213+
214+
async def get(self, dispatch_id: int) -> Dispatch:
215+
"""Get a dispatch.
216+
217+
Args:
218+
dispatch_id: The dispatch_id to get.
219+
220+
Returns:
221+
Dispatch: The dispatch.
222+
"""
223+
request = DispatchGetRequest(id=dispatch_id)
224+
response = await self._stub.GetMicrogridDispatch(request) # type: ignore
225+
return Dispatch.from_protobuf(response)
226+
227+
async def delete(self, dispatch_id: int) -> None:
228+
"""Delete a dispatch.
229+
230+
Args:
231+
dispatch_id: The dispatch_id to delete.
232+
"""
233+
request = DispatchDeleteRequest(id=dispatch_id)
234+
await self._stub.DeleteMicrogridDispatch(request) # type: ignore

0 commit comments

Comments
 (0)