Skip to content

Commit d9e4fda

Browse files
committed
Add support for dispatch_ids and queries filters
Signed-off-by: Mathias L. Baumann <mathias.baumann@frequenz.com>
1 parent 53a310e commit d9e4fda

File tree

5 files changed

+192
-2
lines changed

5 files changed

+192
-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
290+
(e.g., 'bar' matches 'foobar'). 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 to match client.list() parameter type
301+
if "dispatch_ids" in filters:
302+
filters["dispatch_ids"] = iter(filters["dispatch_ids"])
303+
304+
# Convert filter_queries to iterator to match client.list() parameter type
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: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from frequenz.api.dispatch.v1.dispatch_pb2 import (
2222
CreateMicrogridDispatchResponse,
2323
DeleteMicrogridDispatchRequest,
24+
DispatchFilter,
2425
GetMicrogridDispatchRequest,
2526
GetMicrogridDispatchResponse,
2627
ListMicrogridDispatchesRequest,
@@ -178,6 +179,46 @@ def _filter_dispatch(
178179
if dispatch.dry_run != _filter.is_dry_run:
179180
return False
180181

182+
if len(_filter.dispatch_ids) > 0 and dispatch.id not in map(
183+
DispatchId, _filter.dispatch_ids
184+
):
185+
return False
186+
187+
if not FakeService._matches_query(_filter, dispatch):
188+
return False
189+
190+
return True
191+
192+
@staticmethod
193+
def _matches_query(filter_: DispatchFilter, dispatch: Dispatch) -> bool:
194+
"""Check if a dispatch matches the query."""
195+
# Two cases:
196+
# - query starts with # and is interpretable as int: filter by id
197+
# - otherwise: filter by exact match in 'type' field
198+
# Multiple queries use OR logic - dispatch must match at least one
199+
if len(filter_.queries) > 0:
200+
matches_any_query = False
201+
for query in filter_.queries:
202+
if query.startswith("#"):
203+
try:
204+
int_id = int(query[1:])
205+
except ValueError:
206+
# not an int, interpret as exact type match (without the #)
207+
if query[1:] == dispatch.type:
208+
matches_any_query = True
209+
break
210+
else:
211+
query_id = DispatchId(int_id)
212+
if dispatch.id == query_id:
213+
matches_any_query = True
214+
break
215+
elif query == dispatch.type:
216+
matches_any_query = True
217+
break
218+
219+
if not matches_any_query:
220+
return False
221+
181222
return True
182223

183224
async def CreateMicrogridDispatch(

tests/test_client.py

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

115115

116+
# pylint: disable-next=too-many-locals
117+
async def test_list_filter_queries(
118+
client: FakeClient, generator: DispatchGenerator
119+
) -> None:
120+
"""Test listing dispatches with filter queries."""
121+
microgrid_id = MicrogridId(1)
122+
123+
all_dispatches = [generator.generate_dispatch() for _ in range(100)]
124+
client.set_dispatches(
125+
microgrid_id=microgrid_id,
126+
value=all_dispatches,
127+
)
128+
129+
# Filter by type
130+
filter_type = all_dispatches[0].type
131+
dispatches = client.list(
132+
microgrid_id=microgrid_id,
133+
filter_queries=iter([filter_type]),
134+
)
135+
async for page in dispatches:
136+
for dispatch in page:
137+
assert dispatch.type == filter_type
138+
139+
# Filter by id (needs to prefix with #)
140+
filter_target_id = all_dispatches[0].id
141+
dispatches = client.list(
142+
microgrid_id=microgrid_id,
143+
filter_queries=iter([f"#{int(filter_target_id)}"]),
144+
)
145+
async for page in dispatches:
146+
for dispatch in page:
147+
assert dispatch.id == filter_target_id
148+
149+
# Mixed filter - validate OR behavior with type and id
150+
filter_mixed = [all_dispatches[3].type, f"#{int(all_dispatches[4].id)}"]
151+
dispatches = client.list(
152+
microgrid_id=microgrid_id,
153+
filter_queries=iter(filter_mixed),
154+
)
155+
async for page in dispatches:
156+
for dispatch in page:
157+
assert (
158+
dispatch.type == all_dispatches[3].type
159+
or dispatch.id == all_dispatches[4].id
160+
)
161+
162+
# Test OR behavior with multiple types - validate both dispatches are found
163+
type1 = all_dispatches[5].type
164+
type2 = all_dispatches[6].type
165+
dispatches = client.list(
166+
microgrid_id=microgrid_id,
167+
filter_queries=iter([type1, type2]),
168+
)
169+
found_dispatches = []
170+
async for page in dispatches:
171+
for dispatch in page:
172+
assert dispatch.type in (type1, type2)
173+
found_dispatches.append(dispatch)
174+
175+
# Verify both dispatches with the specified types are found
176+
assert len(found_dispatches) == 2
177+
assert any(dispatch.type == type1 for dispatch in found_dispatches)
178+
assert any(dispatch.type == type2 for dispatch in found_dispatches)
179+
180+
# Test OR behavior with multiple IDs - validate both dispatches are found
181+
id1 = all_dispatches[7].id
182+
id2 = all_dispatches[8].id
183+
# Use numeric part only for ID queries (fake service expects just the number)
184+
dispatches = client.list(
185+
microgrid_id=microgrid_id,
186+
filter_queries=iter([f"#{int(id1)}", f"#{int(id2)}"]),
187+
)
188+
found_dispatches = []
189+
async for page in dispatches:
190+
for dispatch in page:
191+
assert dispatch.id in (id1, id2)
192+
found_dispatches.append(dispatch)
193+
194+
# Verify both dispatches with the specified IDs are found
195+
assert len(found_dispatches) == 2
196+
assert any(dispatch.id == id1 for dispatch in found_dispatches)
197+
assert any(dispatch.id == id2 for dispatch in found_dispatches)
198+
199+
# Test OR behavior with mixed types and IDs - validate all dispatches are found
200+
mixed_filter = [
201+
all_dispatches[9].type,
202+
f"#{int(all_dispatches[10].id)}",
203+
all_dispatches[11].type,
204+
f"#{int(all_dispatches[12].id)}",
205+
]
206+
dispatches = client.list(
207+
microgrid_id=microgrid_id,
208+
filter_queries=iter(mixed_filter),
209+
)
210+
found_dispatches = []
211+
async for page in dispatches:
212+
for dispatch in page:
213+
assert (
214+
dispatch.type == all_dispatches[9].type
215+
or dispatch.id == all_dispatches[10].id
216+
or dispatch.type == all_dispatches[11].type
217+
or dispatch.id == all_dispatches[12].id
218+
)
219+
found_dispatches.append(dispatch)
220+
221+
# Verify all four dispatches with the specified types/IDs are found
222+
assert len(found_dispatches) == 4
223+
assert any(dispatch.type == all_dispatches[9].type for dispatch in found_dispatches)
224+
assert any(dispatch.id == all_dispatches[10].id for dispatch in found_dispatches)
225+
assert any(
226+
dispatch.type == all_dispatches[11].type for dispatch in found_dispatches
227+
)
228+
assert any(dispatch.id == all_dispatches[12].id for dispatch in found_dispatches)
229+
230+
116231
async def test_list_dispatches_no_duration(
117232
client: FakeClient, generator: DispatchGenerator
118233
) -> None:

0 commit comments

Comments
 (0)