Skip to content

Commit 770b8dd

Browse files
committed
Implement extra classes for each target type
..and support the latest CategoryAndType target type. This implements extra classes for each target component case: * List of Ids `TargetIds` * List of categories: `TargetCategories` * List of categories + subtypes: `TargetCategoriesAndTypes` This was easier to do together as the addition of the new type increased the places where it was difficult to find out what the actual target type/ids is/are. Signed-off-by: Mathias L. Baumann <[email protected]>
1 parent d981cd8 commit 770b8dd

File tree

9 files changed

+420
-87
lines changed

9 files changed

+420
-87
lines changed

RELEASE_NOTES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Frequenz Dispatch Client Library Release Notes
1+
# Frequenz Dispatch Client Library Release Note
22

33
## Summary
44

pyproject.toml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
requires = [
66
"setuptools == 80.4.0",
77
"setuptools_scm[toml] == 8.3.1",
8-
"frequenz-repo-config[lib] == 0.13.3",
8+
"frequenz-repo-config[lib] == 0.13.4",
99
]
1010
build-backend = "setuptools.build_meta"
1111

@@ -39,6 +39,7 @@ dependencies = [
3939
"typing-extensions >= 4.6.1, < 5",
4040
# "frequenz-api-dispatch == 1.0.0-rc1",
4141
"frequenz-api-dispatch @ git+https://github.com/frequenz-floss/frequenz-api-dispatch@4274d1a",
42+
"frequenz-api-common @ git+https://github.com/frequenz-floss/frequenz-api-common@c75dba3",
4243
"frequenz-client-base >= 0.8.0, < 0.10.0",
4344
"frequenz-client-common >= 0.1.0, < 0.4.0",
4445
"grpcio >= 1.70.0, < 2",
@@ -75,10 +76,11 @@ dev-mkdocs = [
7576
"mike == 2.1.3",
7677
"mkdocs-gen-files == 0.5.0",
7778
"mkdocs-literate-nav == 0.6.2",
79+
"frequenz-api-dispatch @ git+https://github.com/frequenz-floss/frequenz-api-dispatch@4274d1a",
7880
"mkdocs-macros-plugin == 1.3.7",
7981
"mkdocs-material == 9.6.12",
8082
"mkdocstrings[python] == 0.29.1",
81-
"frequenz-repo-config[lib] == 0.13.3",
83+
"frequenz-repo-config[lib] == 0.13.4",
8284
]
8385
dev-mypy = [
8486
"mypy == 1.15.0",
@@ -89,7 +91,7 @@ dev-mypy = [
8991
"types-protobuf == 5.29.1.20250403",
9092
"types-python-dateutil == 2.9.0.20241206",
9193
]
92-
dev-noxfile = ["nox == 2025.5.1", "frequenz-repo-config[lib] == 0.13.3"]
94+
dev-noxfile = ["nox == 2025.5.1", "frequenz-repo-config[lib] == 0.13.4"]
9395
dev-pylint = [
9496
"pylint == 3.3.6",
9597
# For checking the noxfile, docs/ script, and tests
@@ -99,7 +101,7 @@ dev-pylint = [
99101
]
100102
dev-pytest = [
101103
"pytest == 8.3.5",
102-
"frequenz-repo-config[extra-lint-examples] == 0.13.3",
104+
"frequenz-repo-config[extra-lint-examples] == 0.13.4",
103105
"pytest-mock == 3.14.0",
104106
"pytest-asyncio == 0.26.0",
105107
"async-solipsism == 0.7",

src/frequenz/client/dispatch/__main__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ def print_dispatch(dispatch: Dispatch) -> None:
8686
# Format the target
8787
if dispatch.target:
8888
if len(dispatch.target) == 1:
89-
target_str: str = str(dispatch.target[0])
89+
(first_element,) = dispatch.target
90+
target_str: str = str(first_element)
9091
else:
9192
target_str = ", ".join(str(s) for s in dispatch.target)
9293
else:

src/frequenz/client/dispatch/_cli_types.py

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,22 @@
55

66
import json
77
from datetime import datetime, timedelta, timezone
8+
from itertools import chain
89
from typing import Any, Literal, cast
910

1011
import asyncclick as click
1112
import parsedatetime # type: ignore
1213
from tzlocal import get_localzone
1314

1415
from frequenz.client.common.microgrid.components import ComponentCategory
16+
from frequenz.client.dispatch.types import (
17+
BatteryType,
18+
EvChargerType,
19+
InverterType,
20+
TargetCategories,
21+
TargetComponents,
22+
TargetIds,
23+
)
1524

1625
# Disable a false positive from pylint
1726
# pylint: disable=inconsistent-return-statements
@@ -140,7 +149,7 @@ class TargetComponentParamType(click.ParamType):
140149

141150
def convert(
142151
self, value: Any, param: click.Parameter | None, ctx: click.Context | None
143-
) -> list[ComponentCategory] | list[int]:
152+
) -> TargetIds | TargetCategories:
144153
"""Convert the input value into a list of ComponentCategory or IDs.
145154
146155
Args:
@@ -149,9 +158,9 @@ def convert(
149158
ctx: The Click context object.
150159
151160
Returns:
152-
A list of component ids or component categories.
161+
A list of targets, either as component IDs or component categories.
153162
"""
154-
if isinstance(value, list): # Already a list
163+
if isinstance(value, TargetComponents):
155164
return value
156165

157166
values = value.split(",")
@@ -162,20 +171,46 @@ def convert(
162171
error: Exception | None = None
163172
# Attempt to parse component ids
164173
try:
165-
return [int(id) for id in values]
174+
return TargetIds(*[int(id) for id in values])
166175
except ValueError as e:
167176
error = e
168177

178+
def enum_from_str(
179+
name: str,
180+
) -> InverterType | BatteryType | EvChargerType | ComponentCategory:
181+
"""Convert a string to an enum member."""
182+
name = name.strip().upper()
183+
if name in ComponentCategory.__members__:
184+
return ComponentCategory[name]
185+
if name in InverterType.__members__:
186+
return InverterType[name]
187+
if name in BatteryType.__members__:
188+
return BatteryType[name]
189+
if name in EvChargerType.__members__:
190+
return EvChargerType[name]
191+
raise KeyError(f"Invalid target specification: {name}")
192+
169193
# Attempt to parse as component categories, trim whitespace
170194
try:
171-
return [ComponentCategory[cat.strip().upper()] for cat in values]
195+
return TargetCategories(*[enum_from_str(cat) for cat in values])
172196
except KeyError as e:
173197
error = e
174198

199+
types_str = ", ".join(
200+
[f"{type.name}" for type in chain(BatteryType, InverterType, EvChargerType)]
201+
)
202+
175203
self.fail(
176204
f'Invalid component category list or ID list: "{value}".\n'
177205
f'Error: "{error}"\n\n'
178-
"Possible categories: BATTERY, GRID, METER, INVERTER, EV_CHARGER, CHP ",
206+
"Valid formats:\n"
207+
"- 1,2,3 # A list of component IDs\n"
208+
"- METER,INVERTER # A list of component categories\n"
209+
"- NA_ION,SOLAR # A list of component category types (category is derived)\n"
210+
"Valid categories:\n"
211+
f"{', '.join([cat.name for cat in ComponentCategory])}\n"
212+
"Valid types:\n"
213+
f"{types_str}\n",
179214
param,
180215
ctx,
181216
)

src/frequenz/client/dispatch/test/generator.py

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,16 @@
1010

1111
from .._internal_types import rounded_start_time
1212
from ..recurrence import EndCriteria, Frequency, RecurrenceRule, Weekday
13-
from ..types import Dispatch
13+
from ..types import (
14+
BatteryType,
15+
Dispatch,
16+
EvChargerType,
17+
InverterType,
18+
TargetCategories,
19+
TargetCategory,
20+
TargetComponents,
21+
TargetIds,
22+
)
1423

1524

1625
class DispatchGenerator:
@@ -66,6 +75,27 @@ def generate_recurrence_rule(self) -> RecurrenceRule:
6675
],
6776
)
6877

78+
def generate_target_category_and_type(self) -> TargetCategory:
79+
"""Generate a random category and type.
80+
81+
Returns:
82+
a random category and type
83+
"""
84+
category = self._rng.choice(list(ComponentCategory)[1:])
85+
category_type: BatteryType | InverterType | EvChargerType | None = None
86+
87+
match category:
88+
case ComponentCategory.BATTERY:
89+
category_type = self._rng.choice(list(BatteryType)[1:])
90+
case ComponentCategory.INVERTER:
91+
category_type = self._rng.choice(list(InverterType)[1:])
92+
case ComponentCategory.EV_CHARGER:
93+
category_type = self._rng.choice(list(EvChargerType)[1:])
94+
case _:
95+
category_type = None
96+
97+
return TargetCategory(category_type or category)
98+
6999
def generate_dispatch(self) -> Dispatch:
70100
"""Generate a random dispatch instance.
71101
@@ -77,6 +107,20 @@ def generate_dispatch(self) -> Dispatch:
77107
self._rng.randint(0, 1000000), tz=timezone.utc
78108
)
79109

110+
target_choices: list[TargetComponents] = [
111+
TargetIds(
112+
*[self._rng.randint(1, 100) for _ in range(self._rng.randint(1, 10))]
113+
),
114+
TargetCategories(
115+
*[
116+
# Not yet used
117+
# self.generate_target_category_and_type()
118+
self._rng.choice(list(ComponentCategory)[1:])
119+
for _ in range(self._rng.randint(1, 10))
120+
]
121+
),
122+
]
123+
80124
return Dispatch(
81125
id=self._last_id,
82126
create_time=create_time,
@@ -92,18 +136,7 @@ def generate_dispatch(self) -> Dispatch:
92136
timedelta(seconds=self._rng.randint(0, 1000000)),
93137
]
94138
),
95-
target=self._rng.choice( # type: ignore
96-
[
97-
[
98-
self._rng.choice(list(ComponentCategory)[1:])
99-
for _ in range(self._rng.randint(1, 10))
100-
],
101-
[
102-
self._rng.randint(1, 100)
103-
for _ in range(self._rng.randint(1, 10))
104-
],
105-
]
106-
),
139+
target=self._rng.choice(target_choices),
107140
active=self._rng.choice([True, False]),
108141
dry_run=self._rng.choice([True, False]),
109142
payload={

0 commit comments

Comments
 (0)