diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index f80f12e..44299b0 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -10,7 +10,9 @@ ## New Features - +* Add HMAC generation capabilities. + * The new CLI option "key" can be used to provide the server's key. + * The client itself now has a "key" argument in the constructor. ## Bug Fixes diff --git a/pyproject.toml b/pyproject.toml index 2ee496e..665dc02 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ dependencies = [ "frequenz-client-common >= 0.3.0, < 0.4", "grpcio >=1.70.0, < 2", "protobuf >= 5.29.3, < 7", - "frequenz-client-base >= 0.8.0, < 0.11.0", + "frequenz-client-base >= 0.11.0, < 0.12.0", ] dynamic = ["version"] @@ -156,6 +156,10 @@ disable = [ [tool.pytest.ini_options] addopts = "-vv" +testpaths = ["tests", "src"] +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" +required_plugins = ["pytest-asyncio", "pytest-mock"] filterwarnings = [ "error", "once::DeprecationWarning", @@ -164,10 +168,6 @@ filterwarnings = [ # chars as this is a regex 'ignore:Protobuf gencode version .*exactly one major version older.*:UserWarning', ] -testpaths = ["tests", "src"] -asyncio_mode = "auto" -asyncio_default_fixture_loop_scope = "function" -required_plugins = ["pytest-asyncio", "pytest-mock"] [tool.mypy] explicit_package_bases = true diff --git a/src/frequenz/client/reporting/_client.py b/src/frequenz/client/reporting/_client.py index 36537b9..f5e0f1f 100644 --- a/src/frequenz/client/reporting/_client.py +++ b/src/frequenz/client/reporting/_client.py @@ -64,10 +64,13 @@ class ReportingApiClient(BaseApiClient[ReportingStub]): """A client for the Reporting service.""" + # pylint: disable-next=too-many-arguments def __init__( self, server_url: str, - key: str | None = None, + *, + auth_key: str | None = None, + sign_secret: str | None = None, connect: bool = True, channel_defaults: ChannelOptions = ChannelOptions(), # default options ) -> None: @@ -75,7 +78,8 @@ def __init__( Args: server_url: The URL of the Reporting service. - key: The API key for the authorization. + auth_key: The API key for the authorization. + sign_secret: The secret to use for HMAC signing the message connect: Whether to connect to the server immediately. channel_defaults: The default channel options. """ @@ -84,6 +88,8 @@ def __init__( ReportingStub, connect=connect, channel_defaults=channel_defaults, + auth_key=auth_key, + sign_secret=sign_secret, ) self._components_data_streams: dict[ @@ -129,8 +135,6 @@ def __init__( GrpcStreamBroadcaster[PBAggregatedStreamResponse, MetricSample], ] = {} - self._metadata = (("key", key),) if key else () - @property def stub(self) -> ReportingStub: """The gRPC stub for the API.""" @@ -309,7 +313,7 @@ def stream_method() -> ( AsyncIterable[PBReceiveMicrogridComponentsDataStreamResponse] ): call_iterator = self.stub.ReceiveMicrogridComponentsDataStream( - request, metadata=self._metadata + request, ) return cast( AsyncIterable[PBReceiveMicrogridComponentsDataStreamResponse], @@ -493,9 +497,7 @@ def transform_response( def stream_method() -> ( AsyncIterable[PBReceiveMicrogridSensorsDataStreamResponse] ): - call_iterator = self.stub.ReceiveMicrogridSensorsDataStream( - request, metadata=self._metadata - ) + call_iterator = self.stub.ReceiveMicrogridSensorsDataStream(request) return cast( AsyncIterable[PBReceiveMicrogridSensorsDataStreamResponse], call_iterator, @@ -588,7 +590,7 @@ def transform_response( def stream_method() -> AsyncIterable[PBAggregatedStreamResponse]: call_iterator = ( self.stub.ReceiveAggregatedMicrogridComponentsDataStream( - request, metadata=self._metadata + request, ) ) diff --git a/src/frequenz/client/reporting/cli/__main__.py b/src/frequenz/client/reporting/cli/__main__.py index 88f94ae..6f0563b 100644 --- a/src/frequenz/client/reporting/cli/__main__.py +++ b/src/frequenz/client/reporting/cli/__main__.py @@ -79,11 +79,17 @@ def main() -> None: "--format", choices=["iter", "csv", "dict"], help="Output format", default="csv" ) parser.add_argument( - "--key", + "--auth_key", type=str, help="API key", default=None, ) + parser.add_argument( + "--sign_secret", + type=str, + help="The secret to use for generating HMAC signatures", + default=None, + ) args = parser.parse_args() asyncio.run( run( @@ -96,8 +102,9 @@ def main() -> None: states=args.states, bounds=args.bounds, service_address=args.url, - key=args.key, + auth_key=args.key, fmt=args.format, + sign_secret=args.sign_secret, ) ) @@ -114,8 +121,9 @@ async def run( # noqa: DOC502 states: bool, bounds: bool, service_address: str, - key: str, + auth_key: str, fmt: str, + sign_secret: str | None, ) -> None: """Test the ReportingApiClient. @@ -129,13 +137,16 @@ async def run( # noqa: DOC502 states: include states in the output bounds: include bounds in the output service_address: service address - key: API key + auth_key: API key fmt: output format + sign_secret: secret used for creating HMAC signatures Raises: ValueError: if output format is invalid """ - client = ReportingApiClient(service_address, key) + client = ReportingApiClient( + service_address, auth_key=auth_key, sign_secret=sign_secret + ) metrics = [Metric[mn] for mn in metric_names] diff --git a/tests/test_client_reporting.py b/tests/test_client_reporting.py index 1b98834..a054c48 100644 --- a/tests/test_client_reporting.py +++ b/tests/test_client_reporting.py @@ -19,22 +19,27 @@ async def test_client_initialization() -> None: # Parameters for the ReportingApiClient initialization server_url = "gprc://localhost:50051" key = "some-api-key" + sign_secret = "hunter2" connect = True channel_defaults = ChannelOptions() with patch.object(BaseApiClient, "__init__", return_value=None) as mock_base_init: - client = ReportingApiClient( - server_url, key=key, connect=connect, channel_defaults=channel_defaults + ReportingApiClient( + server_url, + auth_key=key, + connect=connect, + channel_defaults=channel_defaults, + sign_secret=sign_secret, ) # noqa: F841 mock_base_init.assert_called_once_with( server_url, ReportingStub, connect=connect, channel_defaults=channel_defaults, + auth_key=key, + sign_secret=sign_secret, ) - assert client._metadata == (("key", key),) # pylint: disable=W0212 - def test_components_data_batch_is_empty_true() -> None: """Test that the is_empty method returns True when the page is empty."""