Skip to content

Commit 90daf45

Browse files
authored
Add support for requesting multiple metrics (#26)
This add support to request multiple metrics in a single request. The limitation to query data only from a single component is still in place. Moreover the distinct return types when querying single or multiple metrics is removed. In addition to that, this turns the example client into a command line tool that can be used to extract data from the reporting API and print it to stdout. Besides option for selecting the data, the service address and the display format can be changed via command line.
2 parents 9b830c8 + 5aec06d commit 90daf45

File tree

3 files changed

+153
-110
lines changed

3 files changed

+153
-110
lines changed

RELEASE_NOTES.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
## Summary
44

55
This release introduces the initial version of the Reporting API client with support for
6-
retrieving single metric historical data for a single component.
6+
retrieving historical metrics data for a single component.
77

88
## Upgrading
99

@@ -12,16 +12,18 @@ retrieving single metric historical data for a single component.
1212
## New Features
1313

1414
* Introducing the initial version of the Reporting API client, streamlined for
15-
retrieving single metric historical data for a single component. It incorporates
15+
retrieving historical metrics data for a single component. It incorporates
1616
pagination handling and utilizes a wrapper data class that retains the raw
1717
protobuf response while offering transformation capabilities limited here
1818
to generators of structured data representation via named tuples.
1919

20-
* Current limitations include a single metric focus with plans for extensibility,
20+
* Current limitations include a single component focus with plans for extensibility,
2121
ongoing development for states and bounds integration, as well as support for
2222
service-side features like resampling, streaming, and formula aggregations.
2323

2424
* Code examples are provided to guide users through the basic usage of the client.
25+
The example client is a simple command-line tool that retrieves historical metrics
26+
data for a single component and prints it to the console.
2527

2628

2729
## Bug Fixes

examples/client.py

Lines changed: 142 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import asyncio
88
from datetime import datetime
99
from pprint import pprint
10-
from typing import AsyncGenerator
10+
from typing import AsyncIterator
1111

1212
import pandas as pd
1313
from frequenz.client.common.metric import Metric
@@ -18,122 +18,166 @@
1818
from frequenz.client.reporting._client import MetricSample
1919

2020

21-
# pylint: disable=too-many-locals
22-
async def main(microgrid_id: int, component_id: int) -> None:
21+
def main() -> None:
22+
"""Parse arguments and run the client."""
23+
parser = argparse.ArgumentParser()
24+
parser.add_argument(
25+
"--url",
26+
type=str,
27+
help="URL of the Reporting service",
28+
default="localhost:50051",
29+
)
30+
parser.add_argument("--mid", type=int, help="Microgrid ID", required=True)
31+
parser.add_argument("--cid", type=int, help="Component ID", required=True)
32+
parser.add_argument(
33+
"--metrics",
34+
type=str,
35+
nargs="+",
36+
choices=[e.name for e in Metric],
37+
help="List of metrics to process",
38+
required=True,
39+
)
40+
parser.add_argument(
41+
"--start",
42+
type=datetime.fromisoformat,
43+
help="Start datetime in YYYY-MM-DDTHH:MM:SS format",
44+
required=True,
45+
)
46+
parser.add_argument(
47+
"--end",
48+
type=datetime.fromisoformat,
49+
help="End datetime in YYYY-MM-DDTHH:MM:SS format",
50+
required=True,
51+
)
52+
parser.add_argument("--psize", type=int, help="Page size", default=100)
53+
parser.add_argument(
54+
"--display", choices=["iter", "df", "dict"], help="Display format", default="df"
55+
)
56+
args = parser.parse_args()
57+
asyncio.run(
58+
run(
59+
args.mid,
60+
args.cid,
61+
args.metrics,
62+
args.start,
63+
args.end,
64+
page_size=args.psize,
65+
service_address=args.url,
66+
display=args.display,
67+
)
68+
)
69+
70+
71+
# pylint: disable=too-many-arguments
72+
async def run(
73+
microgrid_id: int,
74+
component_id: int,
75+
metric_names: list[str],
76+
start_dt: datetime,
77+
end_dt: datetime,
78+
page_size: int,
79+
service_address: str,
80+
display: str,
81+
) -> None:
2382
"""Test the ReportingClient.
2483
2584
Args:
26-
microgrid_id: int
27-
component_id: int
85+
microgrid_id: microgrid ID
86+
component_id: component ID
87+
metric_names: list of metric names
88+
start_dt: start datetime
89+
end_dt: end datetime
90+
page_size: page size
91+
service_address: service address
92+
display: display format
93+
94+
Raises:
95+
ValueError: if display format is invalid
2896
"""
29-
service_address = "localhost:50051"
3097
client = ReportingClient(service_address)
3198

32-
microgrid_components = [(microgrid_id, [component_id])]
33-
metrics = [
34-
Metric.DC_POWER,
35-
Metric.DC_CURRENT,
36-
]
37-
38-
start_dt = datetime.fromisoformat("2023-11-21T12:00:00.00+00:00")
39-
end_dt = datetime.fromisoformat("2023-11-21T12:01:00.00+00:00")
40-
41-
page_size = 10
42-
43-
print("########################################################")
44-
print("Iterate over single metric generator")
45-
46-
async for sample in client.iterate_single_metric(
47-
microgrid_id=microgrid_id,
48-
component_id=component_id,
49-
metric=metrics[0],
50-
start_dt=start_dt,
51-
end_dt=end_dt,
52-
page_size=page_size,
53-
):
54-
print("Received:", sample)
55-
56-
###########################################################################
57-
#
58-
# The following code is experimental and demonstrates potential future
59-
# usage of the ReportingClient.
60-
#
61-
###########################################################################
62-
63-
async def components_data_iter() -> AsyncGenerator[MetricSample, None]:
64-
"""Iterate over components data.
65-
66-
Yields:
67-
Single metric sample
99+
metrics = [Metric[mn] for mn in metric_names]
100+
101+
def data_iter() -> AsyncIterator[MetricSample]:
102+
"""Iterate over single metric.
103+
104+
Just a wrapper around the client method for readability.
105+
106+
Returns:
107+
Iterator over single metric samples
68108
"""
69-
# pylint: disable=protected-access
70-
async for page in client._iterate_components_data_pages(
71-
microgrid_components=microgrid_components,
109+
return client.iterate_single_component(
110+
microgrid_id=microgrid_id,
111+
component_id=component_id,
72112
metrics=metrics,
73113
start_dt=start_dt,
74114
end_dt=end_dt,
75115
page_size=page_size,
76-
):
77-
for entry in page.iterate_metric_samples():
78-
yield entry
79-
80-
async def components_data_dict(
81-
components_data_iter: AsyncGenerator[MetricSample, None]
82-
) -> dict[int, dict[int, dict[datetime, dict[Metric, float]]]]:
83-
"""Convert components data iterator into a single dict.
84-
85-
The nesting structure is:
86-
{
87-
microgrid_id: {
88-
component_id: {
89-
timestamp: {
90-
metric: value
91-
}
116+
)
117+
118+
if display == "iter":
119+
print("########################################################")
120+
print("Iterate over single metric generator")
121+
async for sample in data_iter():
122+
print(sample)
123+
124+
elif display == "dict":
125+
print("########################################################")
126+
print("Dumping all data as a single dict")
127+
dct = await iter_to_dict(data_iter())
128+
pprint(dct)
129+
130+
elif display == "df":
131+
print("########################################################")
132+
print("Turn data into a pandas DataFrame")
133+
data = [cd async for cd in data_iter()]
134+
df = pd.DataFrame(data).set_index("timestamp")
135+
# Set option to display all rows
136+
pd.set_option("display.max_rows", None)
137+
pprint(df)
138+
139+
else:
140+
raise ValueError(f"Invalid display format: {display}")
141+
142+
return
143+
144+
145+
async def iter_to_dict(
146+
components_data_iter: AsyncIterator[MetricSample],
147+
) -> dict[int, dict[int, dict[datetime, dict[Metric, float]]]]:
148+
"""Convert components data iterator into a single dict.
149+
150+
The nesting structure is:
151+
{
152+
microgrid_id: {
153+
component_id: {
154+
timestamp: {
155+
metric: value
92156
}
93157
}
94158
}
159+
}
95160
96-
Args:
97-
components_data_iter: async generator
98-
99-
Returns:
100-
Single dict with with all components data
101-
"""
102-
ret: dict[int, dict[int, dict[datetime, dict[Metric, float]]]] = {}
103-
104-
async for ts, mid, cid, met, value in components_data_iter:
105-
if mid not in ret:
106-
ret[mid] = {}
107-
if cid not in ret[mid]:
108-
ret[mid][cid] = {}
109-
if ts not in ret[mid][cid]:
110-
ret[mid][cid][ts] = {}
111-
112-
ret[mid][cid][ts][met] = value
161+
Args:
162+
components_data_iter: async generator
113163
114-
return ret
164+
Returns:
165+
Single dict with with all components data
166+
"""
167+
ret: dict[int, dict[int, dict[datetime, dict[Metric, float]]]] = {}
115168

116-
print("########################################################")
117-
print("Iterate over generator")
118-
async for msample in components_data_iter():
119-
print("Received:", msample)
169+
async for ts, mid, cid, met, value in components_data_iter:
170+
if mid not in ret:
171+
ret[mid] = {}
172+
if cid not in ret[mid]:
173+
ret[mid][cid] = {}
174+
if ts not in ret[mid][cid]:
175+
ret[mid][cid][ts] = {}
120176

121-
print("########################################################")
122-
print("Dumping all data as a single dict")
123-
dct = await components_data_dict(components_data_iter())
124-
pprint(dct)
177+
ret[mid][cid][ts][met] = value
125178

126-
print("########################################################")
127-
print("Turn data into a pandas DataFrame")
128-
data = [cd async for cd in components_data_iter()]
129-
df = pd.DataFrame(data).set_index("timestamp")
130-
pprint(df)
179+
return ret
131180

132181

133182
if __name__ == "__main__":
134-
parser = argparse.ArgumentParser()
135-
parser.add_argument("microgrid_id", type=int, help="Microgrid ID")
136-
parser.add_argument("component_id", type=int, help="Component ID")
137-
138-
args = parser.parse_args()
139-
asyncio.run(main(args.microgrid_id, args.component_id))
183+
main()

src/frequenz/client/reporting/_client.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,6 @@
3131

3232
# pylint: enable=no-name-in-module
3333

34-
Sample = namedtuple("Sample", ["timestamp", "value"])
35-
"""Type for a sample of a time series."""
36-
3734
MetricSample = namedtuple(
3835
"MetricSample", ["timestamp", "microgrid_id", "component_id", "metric", "value"]
3936
)
@@ -120,22 +117,22 @@ def __init__(self, service_address: str):
120117
self._stub = ReportingStub(self._grpc_channel)
121118

122119
# pylint: disable=too-many-arguments
123-
async def iterate_single_metric(
120+
async def iterate_single_component(
124121
self,
125122
*,
126123
microgrid_id: int,
127124
component_id: int,
128-
metric: Metric,
125+
metrics: Metric | list[Metric],
129126
start_dt: datetime,
130127
end_dt: datetime,
131128
page_size: int = 1000,
132-
) -> AsyncIterator[Sample]:
129+
) -> AsyncIterator[MetricSample]:
133130
"""Iterate over the data for a single metric.
134131
135132
Args:
136133
microgrid_id: The microgrid ID.
137134
component_id: The component ID.
138-
metric: The metric name.
135+
metrics: The metric name or list of metric names.
139136
start_dt: The start date and time.
140137
end_dt: The end date and time.
141138
page_size: The page size.
@@ -147,13 +144,13 @@ async def iterate_single_metric(
147144
"""
148145
async for page in self._iterate_components_data_pages(
149146
microgrid_components=[(microgrid_id, [component_id])],
150-
metrics=[metric],
147+
metrics=[metrics] if isinstance(metrics, Metric) else metrics,
151148
start_dt=start_dt,
152149
end_dt=end_dt,
153150
page_size=page_size,
154151
):
155152
for entry in page.iterate_metric_samples():
156-
yield Sample(timestamp=entry.timestamp, value=entry.value)
153+
yield entry
157154

158155
# pylint: disable=too-many-arguments
159156
async def _iterate_components_data_pages(

0 commit comments

Comments
 (0)