Skip to content

Commit

Permalink
Introduce simple reporting client with support for single metric hist…
Browse files Browse the repository at this point in the history
…orical data (#16)

This initial version of the reporting API client offers focused
functionality to retrieve historical data for a single metric from a
single component.

Features of this version:
* Pagination Handling: Seamlessly request time series data of a single
metric from a single component, with integrated pagination management.
* Data Transformation: Utilize a wrapper data class that retains the
raw protobuf response while offering transformation capabilities. A
generator function is provided for iterating over individual page
values.
* Structured Data: Streamline data representation through named tuples
for timestamp and value pairs, eliminating ambiguity for single
component and metric scenarios.
* Usage Examples: Code examples demonstrate usage, along with
experimental code for upcoming features (considered for removal).
* Unit Testing: Basic unit tests are included.


Limitations of this version:
* Single Metric Focus: Initially supporting queries for individual
metrics to support most common use cases, with an extensible design for
multiple metrics/components in future.
* States and bounds: In line with focus on widely used
functionalities, the approach to integrating states and bounds within
the data structures is still under exploration.
* Metric Sample Variants: Currently supports `SimpleMetricSample`
exclusively, with decision on how to integrate `AggregatedMetricSample`
pending.
* Resampling: not yet exposed (service-side).
* Streaming: Functions not yet available (service-side). Current
generator implementations on pages and entries can be aligned with
streaming output in future.
* Aggregation: Support for formulas still missing (service-side)
  • Loading branch information
cwasicki authored Mar 20, 2024
2 parents 5a0acd1 + 1190207 commit 79726f9
Show file tree
Hide file tree
Showing 6 changed files with 487 additions and 27 deletions.
16 changes: 14 additions & 2 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,27 @@

## Summary

<!-- Here goes a general summary of what this release is about -->
This release introduces the initial version of the Reporting API client with support for
retrieving single metric historical data for a single component.

## Upgrading

<!-- Here goes notes on how to upgrade from previous versions, including deprecations and what they should be replaced with -->

## New Features

<!-- Here goes the main new features and examples or instructions on how to use them -->
* Introducing the initial version of the Reporting API client, streamlined for
retrieving single metric historical data for a single component. It incorporates
pagination handling and utilizes a wrapper data class that retains the raw
protobuf response while offering transformation capabilities limited here
to generators of structured data representation via named tuples.

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

* Code examples are provided to guide users through the basic usage of the client.


## Bug Fixes

Expand Down
139 changes: 139 additions & 0 deletions examples/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# License: MIT
# Copyright © 2024 Frequenz Energy-as-a-Service GmbH

"""Examples usage of reporting API."""

import argparse
import asyncio
from datetime import datetime
from pprint import pprint
from typing import AsyncGenerator

import pandas as pd
from frequenz.client.common.metric import Metric

from frequenz.client.reporting import ReportingClient

# Experimental import
from frequenz.client.reporting._client import MetricSample


# pylint: disable=too-many-locals
async def main(microgrid_id: int, component_id: int) -> None:
"""Test the ReportingClient.
Args:
microgrid_id: int
component_id: int
"""
service_address = "localhost:50051"
client = ReportingClient(service_address)

microgrid_components = [(microgrid_id, [component_id])]
metrics = [
Metric.DC_POWER,
Metric.DC_CURRENT,
]

start_dt = datetime.fromisoformat("2023-11-21T12:00:00.00+00:00")
end_dt = datetime.fromisoformat("2023-11-21T12:01:00.00+00:00")

page_size = 10

print("########################################################")
print("Iterate over single metric generator")

async for sample in client.iterate_single_metric(
microgrid_id=microgrid_id,
component_id=component_id,
metric=metrics[0],
start_dt=start_dt,
end_dt=end_dt,
page_size=page_size,
):
print("Received:", sample)

###########################################################################
#
# The following code is experimental and demonstrates potential future
# usage of the ReportingClient.
#
###########################################################################

async def components_data_iter() -> AsyncGenerator[MetricSample, None]:
"""Iterate over components data.
Yields:
Single metric sample
"""
# pylint: disable=protected-access
async for page in client._iterate_components_data_pages(
microgrid_components=microgrid_components,
metrics=metrics,
start_dt=start_dt,
end_dt=end_dt,
page_size=page_size,
):
for entry in page.iterate_metric_samples():
yield entry

async def components_data_dict(
components_data_iter: AsyncGenerator[MetricSample, None]
) -> dict[int, dict[int, dict[datetime, dict[Metric, float]]]]:
"""Convert components data iterator into a single dict.
The nesting structure is:
{
microgrid_id: {
component_id: {
timestamp: {
metric: value
}
}
}
}
Args:
components_data_iter: async generator
Returns:
Single dict with with all components data
"""
ret: dict[int, dict[int, dict[datetime, dict[Metric, float]]]] = {}

async for ts, mid, cid, met, value in components_data_iter:
if mid not in ret:
ret[mid] = {}
if cid not in ret[mid]:
ret[mid][cid] = {}
if ts not in ret[mid][cid]:
ret[mid][cid][ts] = {}

ret[mid][cid][ts][met] = value

return ret

print("########################################################")
print("Iterate over generator")
async for msample in components_data_iter():
print("Received:", msample)

print("########################################################")
print("Dumping all data as a single dict")
dct = await components_data_dict(components_data_iter())
pprint(dct)

print("########################################################")
print("Turn data into a pandas DataFrame")
data = [cd async for cd in components_data_iter()]
df = pd.DataFrame(data).set_index("timestamp")
pprint(df)


if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("microgrid_id", type=int, help="Microgrid ID")
parser.add_argument("component_id", type=int, help="Component ID")

args = parser.parse_args()
asyncio.run(main(args.microgrid_id, args.component_id))
8 changes: 8 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ requires-python = ">= 3.11, < 4"
# TODO(cookiecutter): Remove and add more dependencies if appropriate
dependencies = [
"typing-extensions >= 4.5.0, < 5",
"frequenz-api-reporting >= 0.1.1, < 1",
"frequenz-client-common @ git+https://github.com/frequenz-floss/[email protected]",
]
dynamic = ["version"]

Expand Down Expand Up @@ -62,6 +64,7 @@ dev-mypy = [
"types-Markdown == 3.4.2.10",
# For checking the noxfile, docs/ script, and tests
"frequenz-client-reporting[dev-mkdocs,dev-noxfile,dev-pytest]",
"pandas-stubs >= 2, < 3", # Only required for example
]
dev-noxfile = [
"nox == 2023.4.22",
Expand All @@ -71,6 +74,7 @@ dev-pylint = [
"pylint == 3.0.2",
# For checking the noxfile, docs/ script, and tests
"frequenz-client-reporting[dev-mkdocs,dev-noxfile,dev-pytest]",
"pandas >= 2, < 3", # Only required for example
]
dev-pytest = [
"pytest == 8.0.0",
Expand All @@ -82,6 +86,10 @@ dev-pytest = [
dev = [
"frequenz-client-reporting[dev-mkdocs,dev-flake8,dev-formatting,dev-mkdocs,dev-mypy,dev-noxfile,dev-pylint,dev-pytest]",
]
examples = [
"grpcio >= 1.51.1, < 2",
"pandas >= 2, < 3",
]

[project.urls]
Documentation = "https://frequenz-floss.github.io/frequenz-client-reporting-python/"
Expand Down
21 changes: 4 additions & 17 deletions src/frequenz/client/reporting/__init__.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,12 @@
# License: MIT
# Copyright © 2024 Frequenz Energy-as-a-Service GmbH

"""Reporting API client for Python.
"""Client to connect to the Reporting API.
TODO(cookiecutter): Add a more descriptive module description.
This package provides a low-level interface for interacting with the reporting API.
"""


# TODO(cookiecutter): Remove this function
def delete_me(*, blow_up: bool = False) -> bool:
"""Do stuff for demonstration purposes.
from ._client import ReportingClient

Args:
blow_up: If True, raise an exception.
Returns:
True if no exception was raised.
Raises:
RuntimeError: if blow_up is True.
"""
if blow_up:
raise RuntimeError("This function should be removed!")
return True
__all__ = ["ReportingClient"]
Loading

0 comments on commit 79726f9

Please sign in to comment.