Skip to content

Commit 4c0f13b

Browse files
committed
Add support for dispatch_ids and queries filters
Signed-off-by: Mathias L. Baumann <[email protected]>
1 parent 8a1e92f commit 4c0f13b

File tree

5 files changed

+103
-2
lines changed

5 files changed

+103
-2
lines changed

RELEASE_NOTES.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@
1010

1111
## New Features
1212

13-
<!-- Here goes the main new features and examples or instructions on how to use them -->
13+
* Added support for `dispatch_ids` and `queries` filters in the `list` method
14+
- `dispatch_ids` parameter allows filtering by specific dispatch IDs
15+
- `filter_queries` parameter supports text-based filtering on dispatch `id` and `type` fields
16+
- Query format: IDs are prefixed with `#` (e.g., `#4`), types are matched as substrings (e.g., `bar` matches `foobar`)
17+
- Multiple queries are combined with logical OR
1418

1519
## Bug Fixes
1620

17-
* The `FakeService` filter list code is now properly checking for unset fields to filter for.

src/frequenz/client/dispatch/__main__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,8 @@ async def cli( # pylint: disable=too-many-arguments, too-many-positional-argume
273273
@click.option("--end-to", type=FuzzyDateTime())
274274
@click.option("--active", type=bool)
275275
@click.option("--dry-run", type=bool)
276+
@click.option("--dispatch-ids", type=int, multiple=True)
277+
@click.option("--filter-queries", type=str, multiple=True)
276278
@click.option("--page-size", type=int)
277279
@click.option("--running", type=bool)
278280
@click.option("--type", "-T", type=str)
@@ -282,6 +284,10 @@ async def list_(ctx: click.Context, /, **filters: Any) -> None:
282284
Lists all dispatches for MICROGRID_ID that match the given filters.
283285
284286
The target option can be given multiple times.
287+
288+
The filter-queries option supports text-based filtering on dispatch id and type fields.
289+
IDs are prefixed with '#' (e.g., '#4'), types are matched as substrings (e.g., 'bar' matches 'foobar').
290+
Multiple queries are combined with logical OR.
285291
"""
286292
filter_running: bool = filters.pop("running", False)
287293
filter_type: str | None = filters.pop("type", None)
@@ -291,6 +297,14 @@ async def list_(ctx: click.Context, /, **filters: Any) -> None:
291297
# Name of the parameter in client.list()
292298
filters["target_components"] = target
293299

300+
# Convert dispatch_ids to iterator if present
301+
if "dispatch_ids" in filters:
302+
filters["dispatch_ids"] = iter(filters["dispatch_ids"])
303+
304+
# Convert filter_queries to iterator if present
305+
if "filter_queries" in filters:
306+
filters["filter_queries"] = iter(filters["filter_queries"])
307+
294308
num_dispatches = 0
295309
num_filtered = 0
296310
async for page in ctx.obj["client"].list(**filters):

src/frequenz/client/dispatch/_client.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,8 @@ async def list(
145145
end_to: datetime | None = None,
146146
active: bool | None = None,
147147
dry_run: bool | None = None,
148+
dispatch_ids: Iterator[DispatchId] = iter(()),
149+
filter_queries: Iterator[str] = iter(()),
148150
page_size: int | None = None,
149151
) -> AsyncIterator[Iterator[Dispatch]]:
150152
"""List dispatches.
@@ -166,6 +168,17 @@ async def list(
166168
print(dispatch)
167169
```
168170
171+
The `filter_queries` parameter is applied to the dispatch `id` and `type` fields.
172+
Each query in the list is applied as a logical OR.
173+
174+
ID tokens are preceded by a `#` so we can tell if an id is intended or a type.
175+
176+
- input of [`#4`] will match only the record with id of `4`
177+
- input of [`#not_an_id`] will match types containing `#not_an_id`
178+
- input of [`bar`] will match `bar` and `foobar`
179+
- input of [`#4`, `#24`, `bar`, `foo`] will match ids of `4` and `24` and
180+
types `foo` `bar` `foobar` `foolish bartender`
181+
169182
Args:
170183
microgrid_id: The microgrid_id to list dispatches for.
171184
target_components: optional, list of component ids or categories to filter by.
@@ -175,6 +188,8 @@ async def list(
175188
end_to: optional, filter by end_time < end_to.
176189
active: optional, filter by active status.
177190
dry_run: optional, filter by dry_run status.
191+
dispatch_ids: optional, list of dispatch IDs to filter by.
192+
filter_queries: optional, list of text queries to filter by.
178193
page_size: optional, number of dispatches to return per page.
179194
180195
Returns:
@@ -203,6 +218,8 @@ def to_interval(
203218
end_time_interval=end_time_interval,
204219
is_active=active,
205220
is_dry_run=dry_run,
221+
dispatch_ids=list(map(int, dispatch_ids)),
222+
queries=list(filter_queries),
206223
)
207224

208225
request = ListMicrogridDispatchesRequest(

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,27 @@ def _filter_dispatch(
178178
if dispatch.dry_run != _filter.is_dry_run:
179179
return False
180180

181+
if len(_filter.dispatch_ids) > 0 and dispatch.id not in map(
182+
DispatchId, _filter.dispatch_ids
183+
):
184+
return False
185+
186+
# Two cases:
187+
# - query starts with # and is interpretable as int: filter by id
188+
# - otherwise: filter by substring in 'type' field
189+
for query in _filter.queries:
190+
if query.startswith("#"):
191+
try:
192+
query_id = DispatchId(int(query[1:]))
193+
if dispatch.id != query_id:
194+
return False
195+
except ValueError:
196+
# not an int, interpret as substring
197+
if query.lower() not in dispatch.type.lower():
198+
return False
199+
elif query.lower() not in dispatch.type.lower():
200+
return False
201+
181202
return True
182203

183204
async def CreateMicrogridDispatch(

tests/test_client.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,52 @@ async def test_list_dispatches(
113113
assert dispatch == service_side_dispatch
114114

115115

116+
async def test_list_filter_queries(
117+
client: FakeClient, generator: DispatchGenerator
118+
) -> None:
119+
"""Test listing dispatches with filter queries."""
120+
microgrid_id = MicrogridId(random.randint(1, 100))
121+
122+
all_dispatches = [generator.generate_dispatch() for _ in range(100)]
123+
client.set_dispatches(
124+
microgrid_id=microgrid_id,
125+
value=all_dispatches,
126+
)
127+
128+
# Filter by type
129+
filter_type = all_dispatches[0].type
130+
dispatches = client.list(
131+
microgrid_id=MicrogridId(1),
132+
filter_queries=iter([filter_type]),
133+
)
134+
async for page in dispatches:
135+
for dispatch in page:
136+
assert dispatch.type == filter_type
137+
138+
# Filter by id (needs to prefix with #)
139+
filter_target_id = all_dispatches[0].id
140+
dispatches = client.list(
141+
microgrid_id=MicrogridId(1),
142+
filter_queries=iter([f"#{filter_target_id}"]),
143+
)
144+
async for page in dispatches:
145+
for dispatch in page:
146+
assert dispatch.id == filter_target_id
147+
148+
# Mixed filter
149+
filter_mixed = [all_dispatches[3].type, f"#{all_dispatches[4].id}"]
150+
dispatches = client.list(
151+
microgrid_id=MicrogridId(1),
152+
filter_queries=iter(filter_mixed),
153+
)
154+
async for page in dispatches:
155+
for dispatch in page:
156+
assert (
157+
dispatch.type == all_dispatches[3].type
158+
or dispatch.id == all_dispatches[4].id
159+
)
160+
161+
116162
async def test_list_dispatches_no_duration(
117163
client: FakeClient, generator: DispatchGenerator
118164
) -> None:

0 commit comments

Comments
 (0)