Skip to content

Commit da75b8f

Browse files
committed
Enhance Assets API client with electrical components support and CLI improvements
This commit introduces several key updates to the Assets API client, including: - New method `list_microgrid_electrical_components()` for retrieving electrical components in a microgrid. - Updated CLI command `assets-cli electrical-components <microgrid-id>` for displaying electrical components. - Improved error handling and type safety in the client. - Enhanced JSON encoding for better serialization of enum keys. - Added new exceptions for better error management. These changes significantly enhance the functionality and usability of the Assets API client within the Frequenz microgrid framework. Signed-off-by: eduardiazf <[email protected]>
1 parent 8a32ad6 commit da75b8f

File tree

6 files changed

+146
-74
lines changed

6 files changed

+146
-74
lines changed

RELEASE_NOTES.md

Lines changed: 13 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,43 +2,24 @@
22

33
## Summary
44

5-
This release introduces a complete Assets API client with CLI support for interacting with Frequenz microgrid assets, including comprehensive error handling and type safety.
6-
7-
## Upgrading
8-
9-
**Breaking Changes:**
10-
11-
- Added new required dependencies: `frequenz-api-assets`, `frequenz-api-common`, `frequenz-client-base`, `grpcio`
12-
13-
**CLI Support:**
14-
Install with `pip install "frequenz-client-assets[cli]"` for command-line functionality.
5+
This release introduces a Assets API client with CLI support for interacting with Frequenz microgrid assets. It provides comprehensive electrical components functionality including batteries, EV chargers, inverters, and grid connection points, with enhanced type safety and error handling.
156

167
## New Features
178

18-
**Assets API Client:**
19-
20-
- Complete gRPC client for Frequenz Assets API
21-
- Extends `BaseApiClient` for authentication and connection management
22-
- `get_microgrid_details()` method for retrieving microgrid information
9+
* **Assets API Client**:
10+
* `list_electrical_components()` method for retrieving electrical components in a microgrid
2311

24-
**Command-Line Interface:**
12+
* **Electrical Components Support**: Comprehensive data classes for electrical components
13+
* `ElectricalComponent` with category-specific information for batteries, EV chargers, inverters, grid connection points, and power transformers
14+
* Battery types: Li-ion, Na-ion with proper enum mapping
15+
* EV charger types: AC, DC, Hybrid charging support
16+
* Operational lifetime tracking and metric configuration bounds
2517

26-
- `python -m frequenz.client.assets microgrid <id>` command
27-
- Environment variable support for API credentials
28-
- JSON output formatting
18+
* **Command-Line Interface**:
19+
* `assets-cli electrical-components <microgrid-id>` command
2920

30-
**Type System:**
31-
32-
- `Microgrid`, `DeliveryArea`, and `Location` data classes
33-
- Protobuf integration with proper type safety
34-
35-
**Exception Handling:**
36-
37-
- Custom exception hierarchy (`AssetsApiError`, `NotFoundError`, `AuthenticationError`, `ServiceUnavailableError`)
38-
- JSON serialization support for error responses
21+
* **Type System**: Enhanced data classes with protobuf integration
22+
* `Microgrid`, `DeliveryArea`, `Location`, and comprehensive electrical component types
23+
* Proper enum mapping: `BatteryType`, `EvChargerType`, `InverterType`, `Metric`
3924

4025
## Bug Fixes
41-
42-
- Improved dependency management with optional dependency groups
43-
- Enhanced gRPC error handling and type safety
44-
- Cleaned up deprecated code

src/frequenz/client/assets/__init__.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,15 @@
44
"""Assets API client."""
55

66
from ._client import AssetsApiClient
7+
from ._delivery_area import DeliveryArea, EnergyMarketCodeType
8+
from ._location import Location
9+
from ._microgrid import Microgrid, MicrogridStatus
710

8-
__all__ = ["AssetsApiClient"]
11+
__all__ = [
12+
"AssetsApiClient",
13+
"DeliveryArea",
14+
"EnergyMarketCodeType",
15+
"Microgrid",
16+
"MicrogridStatus",
17+
"Location",
18+
]

src/frequenz/client/assets/_client.py

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,36 +10,60 @@
1010
from __future__ import annotations
1111

1212
from frequenz.api.assets.v1 import assets_pb2, assets_pb2_grpc
13+
from frequenz.client.base import channel
1314
from frequenz.client.base.client import BaseApiClient, call_stub_method
1415

15-
from frequenz.client.assets.types import Microgrid
16+
from frequenz.client.assets.electrical_component._electrical_component import (
17+
ElectricalComponent,
18+
)
1619

20+
from ._microgrid import Microgrid
21+
from ._microgrid_proto import microgrid_from_proto
22+
from .electrical_component._electrical_component_proto import electrical_component_proto
1723
from .exceptions import ClientNotConnected
1824

25+
DEFAULT_GRPC_CALL_TIMEOUT = 60.0
26+
"""The default timeout for gRPC calls made by this client (in seconds)."""
1927

20-
class AssetsApiClient(BaseApiClient[assets_pb2_grpc.PlatformAssetsStub]):
28+
29+
class AssetsApiClient(
30+
BaseApiClient[assets_pb2_grpc.PlatformAssetsStub]
31+
): # pylint: disable=too-many-arguments
2132
"""A client for the Assets API."""
2233

2334
def __init__(
2435
self,
2536
server_url: str,
26-
auth_key: str | None,
27-
sign_secret: str | None,
37+
*,
38+
auth_key: str | None = None,
39+
sign_secret: str | None = None,
40+
channel_defaults: channel.ChannelOptions = channel.ChannelOptions(),
2841
connect: bool = True,
2942
) -> None:
3043
"""
3144
Initialize the AssetsApiClient.
3245
3346
Args:
34-
server_url: The URL of the server to connect to.
35-
auth_key: The API key to use when connecting to the service.
36-
sign_secret: The secret to use when creating message HMAC.
37-
connect: Whether to connect to the server as soon as a client instance is created.
47+
server_url: The location of the microgrid API server in the form of a URL.
48+
The following format is expected:
49+
"grpc://hostname{:`port`}{?ssl=`ssl`}",
50+
where the `port` should be an int between 0 and 65535 (defaulting to
51+
9090) and `ssl` should be a boolean (defaulting to `true`).
52+
For example: `grpc://localhost:1090?ssl=true`.
53+
auth_key: The authentication key to use for the connection.
54+
sign_secret: The secret to use for signing requests.
55+
channel_defaults: The default options use to create the channel when not
56+
specified in the URL.
57+
connect: Whether to connect to the server as soon as a client instance is
58+
created. If `False`, the client will not connect to the server until
59+
[connect()][frequenz.client.base.client.BaseApiClient.connect] is
60+
called.
3861
"""
3962
super().__init__(
4063
server_url,
4164
assets_pb2_grpc.PlatformAssetsStub,
4265
connect=connect,
66+
channel_defaults=channel_defaults,
4367
auth_key=auth_key,
4468
sign_secret=sign_secret,
4569
)
@@ -61,7 +85,7 @@ def stub(self) -> assets_pb2_grpc.PlatformAssetsAsyncStub:
6185
# use the async stub, so we cast the sync stub to the async stub.
6286
return self._stub # type: ignore
6387

64-
async def get_microgrid_details( # noqa: DOC502 (raises ApiClientError indirectly)
88+
async def get_microgrid( # noqa: DOC502 (raises ApiClientError indirectly)
6589
self, microgrid_id: int
6690
) -> Microgrid:
6791
"""
@@ -77,11 +101,40 @@ async def get_microgrid_details( # noqa: DOC502 (raises ApiClientError indirect
77101
ApiClientError: If there are any errors communicating with the Assets API,
78102
most likely a subclass of [GrpcError][frequenz.client.base.exception.GrpcError].
79103
"""
80-
request = assets_pb2.GetMicrogridRequest(microgrid_id=microgrid_id)
81104
response = await call_stub_method(
82105
self,
83-
lambda: self.stub.GetMicrogrid(request),
106+
lambda: self.stub.GetMicrogrid(
107+
assets_pb2.GetMicrogridRequest(microgrid_id=microgrid_id),
108+
timeout=DEFAULT_GRPC_CALL_TIMEOUT,
109+
),
84110
method_name="GetMicrogrid",
85111
)
86112

87-
return Microgrid.from_protobuf(response.microgrid)
113+
return microgrid_from_proto(response.microgrid)
114+
115+
async def list_microgrid_electrical_components(
116+
self, microgrid_id: int
117+
) -> list[ElectricalComponent]:
118+
"""
119+
Get the electrical components of a microgrid.
120+
121+
Args:
122+
microgrid_id: The ID of the microgrid to get the electrical components of.
123+
124+
Returns:
125+
The electrical components of the microgrid.
126+
"""
127+
response = await call_stub_method(
128+
self,
129+
lambda: self.stub.ListMicrogridElectricalComponents(
130+
assets_pb2.ListMicrogridElectricalComponentsRequest(
131+
microgrid_id=microgrid_id,
132+
),
133+
timeout=DEFAULT_GRPC_CALL_TIMEOUT,
134+
),
135+
method_name="ListMicrogridElectricalComponents",
136+
)
137+
138+
return [
139+
electrical_component_proto(component) for component in response.components
140+
]

src/frequenz/client/assets/_utils.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,22 +42,22 @@ def default(self, o: Any) -> Any:
4242

4343
return super().default(o)
4444

45-
def _encode(self, o: Any) -> Any:
45+
def _encode_containers_recursively(self, o: Any) -> Any:
4646
"""Recursively process objects to convert enum keys to their values."""
4747
if isinstance(o, dict):
4848
return {
4949
(
50-
self._encode(key.value)
50+
self._encode_containers_recursively(key.value)
5151
if isinstance(key, enum.Enum)
52-
else self._encode(key)
53-
): self._encode(value)
52+
else self._encode_containers_recursively(key)
53+
): self._encode_containers_recursively(value)
5454
for key, value in o.items()
5555
}
56-
if isinstance(o, (list, tuple, set, frozenset)):
57-
items = [self._encode(item) for item in o]
56+
elif isinstance(o, (list, tuple, set, frozenset)):
57+
items = [self._encode_containers_recursively(item) for item in o]
5858
return items if isinstance(o, (list, tuple)) else items
5959
return o
6060

6161
def encode(self, o: Any) -> str:
6262
"""Encode the given object to a JSON string, handling enum keys."""
63-
return super().encode(self._encode(o))
63+
return super().encode(self._encode_containers_recursively(o))

src/frequenz/client/assets/cli/__main__.py

Lines changed: 49 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -34,33 +34,13 @@
3434

3535
import asyncio
3636
import json
37-
from dataclasses import asdict
3837

3938
import asyncclick as click
4039

4140
from frequenz.client.assets._client import AssetsApiClient
4241
from frequenz.client.assets.exceptions import ApiClientError
43-
from frequenz.client.assets.types import Microgrid
4442

45-
46-
def print_microgrid_details(microgrid: Microgrid) -> None:
47-
"""
48-
Print microgrid details to console in JSON format.
49-
50-
This function converts the Microgrid instance to a dictionary and
51-
outputs it as formatted JSON to the console. The output is designed
52-
to be machine-readable and can be piped to tools like jq for further
53-
processing.
54-
55-
Args:
56-
microgrid: The Microgrid instance to print to console.
57-
"""
58-
microgrid_dict = asdict(microgrid)
59-
microgrid_dict["id"] = int(microgrid.id)
60-
microgrid_dict["enterprise_id"] = int(microgrid.enterprise_id)
61-
microgrid_dict["create_time"] = microgrid.create_time.isoformat()
62-
63-
click.echo(json.dumps(microgrid_dict, indent=2))
43+
from ._utils import print_electrical_components, print_microgrid_details
6444

6545

6646
@click.group(invoke_without_command=True)
@@ -161,7 +141,7 @@ async def get_microgrid(
161141
"""
162142
try:
163143
client = ctx.obj["client"]
164-
microgrid_details = await client.get_microgrid_details(microgrid_id)
144+
microgrid_details = await client.get_microgrid(microgrid_id)
165145
print_microgrid_details(microgrid_details)
166146
except ApiClientError as e:
167147
error_dict = {
@@ -174,6 +154,53 @@ async def get_microgrid(
174154
raise click.Abort()
175155

176156

157+
@cli.command("electrical-components")
158+
@click.pass_context
159+
@click.argument("microgrid-id", required=True, type=int)
160+
async def list_microgrid_electrical_components(
161+
ctx: click.Context,
162+
microgrid_id: int,
163+
) -> None:
164+
"""
165+
Get and display electrical components by microgrid ID.
166+
167+
This command fetches detailed information about all electrical components
168+
in a specific microgrid from the Assets API and displays it in JSON format.
169+
The output can be piped to other tools for further processing.
170+
171+
Args:
172+
ctx: Click context object containing the initialized API client.
173+
microgrid_id: The unique identifier of the microgrid to retrieve.
174+
175+
Raises:
176+
click.Abort: If there is an error printing the electrical components.
177+
178+
Example:
179+
```bash
180+
# Get details for microgrid with ID 123
181+
assets-cli electrical-components 123
182+
183+
# Pipe output to jq for filtering
184+
assets-cli electrical-components 123 | jq ".id"
185+
```
186+
"""
187+
try:
188+
client = ctx.obj["client"]
189+
electrical_components = await client.list_microgrid_electrical_components(
190+
microgrid_id
191+
)
192+
print_electrical_components(electrical_components)
193+
except ApiClientError as e:
194+
error_dict = {
195+
"error_type": type(e).__name__,
196+
"server_url": e.server_url,
197+
"operation": e.operation,
198+
"description": e.description,
199+
}
200+
click.echo(json.dumps(error_dict, indent=2))
201+
raise click.Abort()
202+
203+
177204
def main() -> None:
178205
"""
179206
Initialize and run the CLI application.

src/frequenz/client/assets/exceptions.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,5 @@
4747
"ServiceUnavailable",
4848
"UnknownError",
4949
"UnrecognizedGrpcStatus",
50+
"PermissionDenied",
5051
]

0 commit comments

Comments
 (0)