From 1034637df25dcaeb0ce813a8f92627562501fc7c Mon Sep 17 00:00:00 2001 From: "Mathias L. Baumann" Date: Thu, 4 May 2023 16:58:01 +0200 Subject: [PATCH 1/2] Add module to validate ``` python wrapped code examples in docstrings Signed-off-by: Mathias L. Baumann --- pyproject.toml | 2 + src/conftest.py | 154 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 src/conftest.py diff --git a/pyproject.toml b/pyproject.toml index 9d169ae40..1433c22e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,6 +75,7 @@ pytest = [ "pytest-asyncio == 0.21.0", "time-machine == 2.9.0", "async-solipsism == 0.5", + "sybil ~= 5.0.0", ] mypy = [ "mypy == 1.2.0", @@ -86,6 +87,7 @@ pylint = [ "pylint == 2.17.3", # For checking the noxfile, docs/ script, and tests "frequenz-sdk[docs-gen,nox,pytest]", + "sybil ~= 5.0.0", ] dev = [ "frequenz-sdk[docs-gen,docs-lint,format,nox,pytest,mypy,pylint]", diff --git a/src/conftest.py b/src/conftest.py new file mode 100644 index 000000000..10d0aae2b --- /dev/null +++ b/src/conftest.py @@ -0,0 +1,154 @@ +# License: MIT +# Copyright © 2022 Frequenz Energy-as-a-Service GmbH + +"""Pytest plugin to validate and run docstring code examples. + +Code examples are often wrapped in triple backticks (```) within our docstrings. +This plugin extracts these code examples and validates them using pylint. +""" + +from __future__ import annotations + +import ast +import os +import subprocess + +from sybil import Sybil +from sybil.evaluators.python import pad +from sybil.parsers.myst import CodeBlockParser + + +def get_import_statements(code: str) -> list[str]: + """Get all import statements from a given code string. + + Args: + code: The code to extract import statements from. + + Returns: + A list of import statements. + """ + tree = ast.parse(code) + import_statements = [] + + for node in ast.walk(tree): + if isinstance(node, (ast.Import, ast.ImportFrom)): + import_statement = ast.get_source_segment(code, node) + import_statements.append(import_statement) + + return import_statements + + +def path_to_import_statement(path: str) -> str: + """Convert a path to a Python file to an import statement. + + Args: + path: The path to convert. + + Returns: + The import statement. + """ + if not path.endswith(".py"): + raise ValueError("Path must point to a Python file (.py)") + + # Remove 'src/' prefix if present + if path.startswith("src/"): + path = path[4:] + + # Remove the '.py' extension and replace '/' with '.' + module_path = path[:-3].replace("/", ".") + + # Create the import statement + import_statement = f"from {module_path} import *" + return import_statement + + +class CustomPythonCodeBlockParser(CodeBlockParser): + """Code block parser that validates extracted code examples using pylint. + + This parser is a modified version of the default Python code block parser + from the Sybil library. + It uses pylint to validate the extracted code examples. + + All code examples are preceded by the original file's import statements as + well as an wildcard import of the file itself. + This allows us to use the code examples as if they were part of the original + file. + + Additionally, the code example is padded with empty lines to make sure the + line numbers are correct. + + Pylint warnings which are unimportant for code examples are disabled. + """ + + def __init__(self): + super().__init__("python", None) + + def evaluate(self, example) -> None | str: + """Validate the extracted code example using pylint. + + Args: + example: The extracted code example. + + Returns: + None if the code example is valid, otherwise the pylint output. + """ + # Get the import statements for the original file + import_statements = get_import_statements(example.document.text) + # Add a wildcard import of the original file + import_statements.append( + path_to_import_statement(os.path.relpath(example.path)) + ) + imports_code = "\n".join(import_statements) + + example_with_imports = f"{imports_code}\n\n{example.parsed}" + + # Make sure the line numbers are correct (unfortunately, this is not + # exactly correct, but good enough to find the line in question) + source = pad( + example_with_imports, + example.line + example.parsed.line_offset - len(import_statements), + ) + + try: + # pylint disable parameters with descriptions + pylint_disable_params = [ + "C0114", # Missing module docstring + "C0115", # Missing class docstring + "C0116", # Missing function or method docstring + "W0401", # Wildcard import + "W0404", # Reimport + "W0611", # Unused import + "W0612", # Unused variable + "W0614", # Unused import from wildcard + "E0611", # No name in module + "E1142", # Await used outside async function + ] + + pylint_command = [ + "pylint", + "--disable", + ",".join(pylint_disable_params), + "--from-stdin", + example.path, + ] + + subprocess.run( + pylint_command, + input=source, + text=True, + capture_output=True, + check=True, + ) + except subprocess.CalledProcessError as exception: + return ( + f"Pylint validation failed for code example:\n" + f"{example_with_imports}\nError: {exception}\nOutput: {exception.output}" + ) + + return None + + +pytest_collect_file = Sybil( + parsers=[CustomPythonCodeBlockParser()], + patterns=["*.py"], +).pytest() From e4356e294fc300239b95eade0e61dc582ea38aa9 Mon Sep 17 00:00:00 2001 From: "Mathias L. Baumann" Date: Tue, 25 Apr 2023 19:49:53 +0200 Subject: [PATCH 2/2] Fix docstring code example problems Signed-off-by: Mathias L. Baumann --- src/frequenz/sdk/actor/_decorator.py | 74 ++++++++------- .../power_distributing/power_distributing.py | 84 ++++++++++------- .../sdk/power/_distribution_algorithm.py | 92 ++++++++++--------- .../_formula_engine/_formula_engine.py | 9 +- src/frequenz/sdk/timeseries/_moving_window.py | 9 +- src/frequenz/sdk/timeseries/_resampling.py | 4 +- .../timeseries/battery_pool/_result_types.py | 20 ++-- .../logical_meter/_logical_meter.py | 25 +++-- 8 files changed, 189 insertions(+), 128 deletions(-) diff --git a/src/frequenz/sdk/actor/_decorator.py b/src/frequenz/sdk/actor/_decorator.py index dd105b432..73018c372 100644 --- a/src/frequenz/sdk/actor/_decorator.py +++ b/src/frequenz/sdk/actor/_decorator.py @@ -76,7 +76,9 @@ def actor(cls: Type[Any]) -> Type[Any]: TypeError: when the class doesn't have a `run` method as per spec. Example (one actor receiving from two receivers): - ``` python + ```python + from frequenz.channels import Receiver, Sender, Broadcast + from frequenz.channels.util import Select @actor class EchoActor: def __init__( @@ -101,25 +103,30 @@ async def run(self) -> None: await self._output.send(msg.inner) - input_chan_1: Broadcast[bool] = Broadcast("input_chan_1") - input_chan_2: Broadcast[bool] = Broadcast("input_chan_2") + async def main() -> None: + input_chan_1: Broadcast[bool] = Broadcast("input_chan_1") + input_chan_2: Broadcast[bool] = Broadcast("input_chan_2") - echo_chan: Broadcast[bool] = Broadcast("EchoChannel") + echo_chan: Broadcast[bool] = Broadcast("EchoChannel") - echo_actor = EchoActor( - "EchoActor", - recv1=input_chan_1.new_receiver(), - recv2=input_chan_2.new_receiver(), - output=echo_chan.new_sender(), - ) - echo_rx = echo_chan.new_receiver() + echo_actor = EchoActor( + "EchoActor", + recv1=input_chan_1.new_receiver(), + recv2=input_chan_2.new_receiver(), + output=echo_chan.new_sender(), + ) + echo_rx = echo_chan.new_receiver() + + await input_chan_2.new_sender().send(True) + msg = await echo_rx.receive() - await input_chan_2.new_sender().send(True) - msg = await echo_rx.receive() + asyncio.run(main()) ``` Example (two Actors composed): - ``` python + ```python + from frequenz.channels import Receiver, Sender, Broadcast + from frequenz.channels.util import Select @actor class Actor1: def __init__( @@ -154,24 +161,27 @@ async def run(self) -> None: await self._output.send(msg) - input_chan: Broadcast[bool] = Broadcast("Input to A1") - a1_chan: Broadcast[bool] = Broadcast["A1 stream"] - a2_chan: Broadcast[bool] = Broadcast["A2 stream"] - a1 = Actor1( - name="ActorOne", - recv=input_chan.new_receiver(), - output=a1_chan.new_sender(), - ) - a2 = Actor2( - name="ActorTwo", - recv=a1_chan.new_receiver(), - output=a2_chan.new_sender(), - ) - - a2_rx = a2_chan.new_receiver() - - await input_chan.new_sender().send(True) - msg = await a2_rx.receive() + async def main() -> None: + input_chan: Broadcast[bool] = Broadcast("Input to A1") + a1_chan: Broadcast[bool] = Broadcast("A1 stream") + a2_chan: Broadcast[bool] = Broadcast("A2 stream") + a_1 = Actor1( + name="ActorOne", + recv=input_chan.new_receiver(), + output=a1_chan.new_sender(), + ) + a_2 = Actor2( + name="ActorTwo", + recv=a1_chan.new_receiver(), + output=a2_chan.new_sender(), + ) + + a2_rx = a2_chan.new_receiver() + + await input_chan.new_sender().send(True) + msg = await a2_rx.receive() + + asyncio.run(main()) ``` """ diff --git a/src/frequenz/sdk/actor/power_distributing/power_distributing.py b/src/frequenz/sdk/actor/power_distributing/power_distributing.py index d855c1091..b421d5c15 100644 --- a/src/frequenz/sdk/actor/power_distributing/power_distributing.py +++ b/src/frequenz/sdk/actor/power_distributing/power_distributing.py @@ -89,11 +89,13 @@ class PowerDistributingActor: printed. Example: - ``` python + ```python import grpc.aio as grpcaio - from frequenz.sdk.microgrid.graph import _MicrogridComponentGraph + from frequenz.sdk.microgrid._graph import _MicrogridComponentGraph + from frequenz.sdk import microgrid from frequenz.sdk.microgrid.component import ComponentCategory + from frequenz.sdk.actor import ResamplerConfig from frequenz.sdk.actor.power_distributing import ( PowerDistributingActor, Request, @@ -103,42 +105,60 @@ class PowerDistributingActor: PartialFailure, Ignored, ) + from frequenz.channels import Bidirectional, Broadcast, Receiver, Sender + from datetime import timedelta + from frequenz.sdk import actor + + HOST = "localhost" + PORT = 50051 + + async def main() -> None: + await microgrid.initialize( + HOST, + PORT, + ResamplerConfig(resampling_period=timedelta(seconds=1)) + ) + + graph = microgrid.connection_manager.get().component_graph + batteries = graph.components(component_category={ComponentCategory.BATTERY}) + batteries_ids = {c.component_id for c in batteries} - target = f"{host}:{port}" - grpc_channel = grpcaio.insecure_channel(target) - api = MicrogridGrpcClient(grpc_channel, target) + battery_status_channel = Broadcast[BatteryStatus]("battery-status") - graph = _MicrogridComponentGraph() - await graph.refresh_from_api(api) + channel = Bidirectional[Request, Result]("user1", "power_distributor") + power_distributor = PowerDistributingActor( + users_channels={"user1": channel.service_handle}, + battery_status_sender=battery_status_channel.new_sender(), + ) - batteries = graph.components(component_category={ComponentCategory.BATTERY}) - batteries_ids = {c.component_id for c in batteries} + # Start the actor + await actor.run(power_distributor) - channel = Bidirectional[Request, Result]("user1", "power_distributor") - power_distributor = PowerDistributingActor( - mock_api, component_graph, {"user1": channel.service_handle} - ) + client_handle = channel.client_handle - client_handle = channel.client_handle - - # Set power 1200W to given batteries. - request = Request(power=1200.0, batteries=batteries_ids, request_timeout_sec=10.0) - await client_handle.send(request) - - # It is recommended to use timeout when waiting for the response! - result: Result = await asyncio.wait_for(client_handle.receive(), timeout=10) - - if isinstance(result, Success): - print("Command succeed") - elif isinstance(result, PartialFailure): - print( - f"Batteries {result.failed_batteries} failed, total failed power" \ - f"{result.failed_power}") - elif isinstance(result, Ignored): - print(f"Request was ignored, because of newer request") - elif isinstance(result, Error): - print(f"Request failed with error: {result.msg}") + # Set power 1200W to given batteries. + request = Request(power=1200.0, batteries=batteries_ids, request_timeout_sec=10.0) + await client_handle.send(request) + + # Set power 1200W to given batteries. + request = Request(power=1200, batteries=batteries_ids, request_timeout_sec=10.0) + await client_handle.send(request) + + # It is recommended to use timeout when waiting for the response! + result: Result = await asyncio.wait_for(client_handle.receive(), timeout=10) + + if isinstance(result, Success): + print("Command succeed") + elif isinstance(result, PartialFailure): + print( + f"Batteries {result.failed_batteries} failed, total failed power" \ + f"{result.failed_power}" + ) + elif isinstance(result, Ignored): + print("Request was ignored, because of newer request") + elif isinstance(result, Error): + print(f"Request failed with error: {result.msg}") ``` """ diff --git a/src/frequenz/sdk/power/_distribution_algorithm.py b/src/frequenz/sdk/power/_distribution_algorithm.py index b7796b6fc..c50df54fb 100644 --- a/src/frequenz/sdk/power/_distribution_algorithm.py +++ b/src/frequenz/sdk/power/_distribution_algorithm.py @@ -71,26 +71,26 @@ class DistributionAlgorithm: We would like our distribution to meet the equation: - ``` python + ``` distribution[i] = power_w * capacity_ratio[i] * x[i] ``` where: - ``` python + ``` sum(capacity_ratio[i] * x[i] for i in range(N)) == 1 ``` Let `y` be our unknown, the proportion to discharge each battery would be (1): - ``` python + ``` x[i] = available_soc[i]*y ``` We can compute `y` from equation above (2): - ``` python + ``` sum(capacity_ratio[i] * x[i] for i in range(N)) == 1 # => sum(capacity_ratio[i] * available_soc[i] * y for i in range(N)) == 1 @@ -100,7 +100,7 @@ class DistributionAlgorithm: Now we know everything and we can compute distribution: - ``` python + ``` distribution[i] = power_w * capacity_ratio[i] * x[i] # from (1) distribution[i] = \ power_w * capacity_ratio[i] * available_soc[i] * y # from (2) @@ -110,13 +110,13 @@ class DistributionAlgorithm: Let: - ``` python + ``` battery_availability_ratio[i] = capacity_ratio[i] * available_soc[i] total_battery_availability_ratio = sum(battery_availability_ratio) ``` Then: - ``` python + ``` distribution[i] = power_w * battery_availability_ratio[i] \ / total_battery_availability_ratio ``` @@ -151,29 +151,33 @@ def __init__(self, distributor_exponent: float = 1) -> None: If `distribution_exponent` is: * `0`: distribution for each battery will be the equal. - ``` python - Bat1.distribution = 4000; Bat2.distribution = 4000 + ```python + BAT1_DISTRIBUTION = 4000 + BAT2_DISTRIBUTION = 4000 ``` * `1`: then `Bat2` will have 3x more power assigned then `Bat1`. - ``` python - 10 * x + 30 * x = 8000 - x = 200 - Bat1.distribution = 2000; Bat2.distribution = 6000 + ```python + # 10 * x + 30 * x = 8000 + X = 200 + BAT1_DISTRIBUTION = 2000 + BAT2_DISTRIBUTION = 6000 ``` * `2`: then `Bat2` will have 9x more power assigned then `Bat1`. - ``` python - 10^2 * x + 30^2 * x = 8000 - x = 80 - Bat1.distribution = 800; Bat2.distribution = 7200 + ```python + # 10^2 * x + 30^2 * x = 8000 + X = 80 + BAT1_DISTRIBUTION = 800 + BAT2_DISTRIBUTION = 7200 ``` * `3`: then `Bat2` will have 27x more power assigned then `Bat1`. - ``` python - 10^3 * x + 30^3 * x = 8000 - x = 0,285714286 - Bat1.distribution = 285; Bat2.distribution = 7715 + ```python + # 10^3 * x + 30^3 * x = 8000 + X = 0.285714286 + BAT1_DISTRIBUTION = 285 + BAT2_DISTRIBUTION = 7715 ``` # Example 2 @@ -189,29 +193,33 @@ def __init__(self, distributor_exponent: float = 1) -> None: If `distribution_exponent` is: * `0`: distribution for each battery will be the same. - ``` python - Bat1.distribution = 4500; Bat2.distribution = 450 + ```python + BAT1_DISTRIBUTION = 4500 + BAT2_DISTRIBUTION = 450 ``` * `1`: then `Bat2` will have 2x more power assigned then `Bat1`. - ``` python - 30 * x + 60 * x = 900 - x = 100 - Bat1.distribution = 300; Bat2.distribution = 600 + ```python + # 30 * x + 60 * x = 900 + X = 100 + BAT1_DISTRIBUTION = 300 + BAT2_DISTRIBUTION = 600 ``` * `2`: then `Bat2` will have 4x more power assigned then `Bat1`. - ``` python - 30^2 * x + 60^2 * x = 900 - x = 0.2 - Bat1.distribution = 180; Bat2.distribution = 720 + ```python + # 30^2 * x + 60^2 * x = 900 + X = 0.2 + BAT1_DISTRIBUTION = 180 + BAT2_DISTRIBUTION = 720 ``` * `3`: then `Bat2` will have 8x more power assigned then `Bat1`. - ``` python - 30^3 * x + 60^3 * x = 900 - x = 0,003703704 - Bat1.distribution = 100; Bat2.distribution = 800 + ```python + # 30^3 * x + 60^3 * x = 900 + X = 0.003703704 + BAT1_DISTRIBUTION = 100 + BAT2_DISTRIBUTION = 800 ``` # Example 3 @@ -226,15 +234,17 @@ def __init__(self, distributor_exponent: float = 1) -> None: If `distribution_exponent` is: * `0`: distribution for each battery will be the equal. - ``` python - Bat1.distribution = 450; Bat2.distribution = 450 + ```python + BAT1_DISTRIBUTION = 450 + BAT2_DISTRIBUTION = 450 ``` * `0.5`: then `Bat2` will have 6/4x more power assigned then `Bat1`. - ``` python - sqrt(36) * x + sqrt(16) * x = 900 - x = 100 - Bat1.distribution = 600; Bat2.distribution = 400 + ```python + # sqrt(36) * x + sqrt(16) * x = 900 + X = 100 + BAT1_DISTRIBUTION = 600 + BAT2_DISTRIBUTION = 400 ``` Raises: diff --git a/src/frequenz/sdk/timeseries/_formula_engine/_formula_engine.py b/src/frequenz/sdk/timeseries/_formula_engine/_formula_engine.py index fd3383b8a..1f428ddb6 100644 --- a/src/frequenz/sdk/timeseries/_formula_engine/_formula_engine.py +++ b/src/frequenz/sdk/timeseries/_formula_engine/_formula_engine.py @@ -409,10 +409,13 @@ class FormulaBuilder: following calls need to be made: ```python - builder = FormulaBuilder() - builder.push_metric("metric_1", receiver_1) + channel = Broadcast[Sample]("channel") + receiver_1 = channel.new_receiver("receiver_1") + receiver_2 = channel.new_receiver("receiver_2") + builder = FormulaBuilder("addition") + builder.push_metric("metric_1", receiver_1, nones_are_zeros=True) builder.push_oper("+") - builder.push_metric("metric_2", receiver_2) + builder.push_metric("metric_2", receiver_2, nones_are_zeros=True) engine = builder.build() ``` diff --git a/src/frequenz/sdk/timeseries/_moving_window.py b/src/frequenz/sdk/timeseries/_moving_window.py index 5be8a5e34..a34bcc015 100644 --- a/src/frequenz/sdk/timeseries/_moving_window.py +++ b/src/frequenz/sdk/timeseries/_moving_window.py @@ -57,6 +57,9 @@ class MovingWindow: Example: Calculate the mean of a time interval ```python + from datetime import datetime, timedelta, timezone + resampled_data_recv = Broadcast[Sample]("sample-data").new_receiver() + window = MovingWindow( size=timedelta(minutes=5), resampled_data_recv=resampled_data_recv, @@ -78,7 +81,11 @@ class MovingWindow: Example: Create a polars data frame from a `MovingWindow` ```python + # pylint: disable=import-error import polars as pl + from datetime import datetime, timedelta, timezone + + sample_receiver = Broadcast[Sample]("sample-data").new_receiver() # create a window that stores two days of data # starting at 1.1.23 with samplerate=1 @@ -94,7 +101,7 @@ class MovingWindow: # create a polars series with one full day of data time_start = datetime(2023, 1, 1, tzinfo=timezone.utc) time_end = datetime(2023, 1, 2, tzinfo=timezone.utc) - s = pl.Series("Jan_1", mv[time_start:time_end]) + s = pl.Series("Jan_1", window[time_start:time_end]) ``` """ diff --git a/src/frequenz/sdk/timeseries/_resampling.py b/src/frequenz/sdk/timeseries/_resampling.py index f77ec1123..7bff71057 100644 --- a/src/frequenz/sdk/timeseries/_resampling.py +++ b/src/frequenz/sdk/timeseries/_resampling.py @@ -59,9 +59,9 @@ This should be an `async` callable, for example: -``` python +```python async some_sink(Sample) -> None: - ... + pass ``` Args: diff --git a/src/frequenz/sdk/timeseries/battery_pool/_result_types.py b/src/frequenz/sdk/timeseries/battery_pool/_result_types.py index 769a6e595..2bdd2c02e 100644 --- a/src/frequenz/sdk/timeseries/battery_pool/_result_types.py +++ b/src/frequenz/sdk/timeseries/battery_pool/_result_types.py @@ -30,17 +30,17 @@ class CapacityMetrics: """Total batteries capacity. Calculated with the formula: - ``` + ```python working_batteries: Set[BatteryData] # working batteries from the battery pool total_capacity = sum(battery.capacity for battery in working_batteries) ``` """ bound: Bound """Capacity bounds. - + Bounds are calculated with the formula: - ``` - working_batteries: Set[BatteryData] # working batteries from the battery + ```python + working_batteries: Set[BatteryData] # working batteries from the battery bound.lower = sum( battery.capacity * battery.soc_lower_bound for battery in working_batteries) @@ -62,7 +62,7 @@ class SoCMetrics: """Average soc. Average soc is calculated with the formula: - ``` + ```python working_batteries: Set[BatteryData] # working batteries from the battery pool used_capacity = sum(battery.capacity * battery.soc for battery in working_batteries) @@ -73,16 +73,16 @@ class SoCMetrics: bound: Bound """SoC bounds weighted by capacity. - + Bounds are calculated with the formula: capacity_lower_bound = sum( battery.capacity * battery.soc_lower_bound for battery in working_batteries) - + capacity_upper_bound = sum( battery.capacity * battery.soc_upper_bound for battery in working_batteries) total_capacity = sum(battery.capacity for battery in working_batteries) - + bound.lower = capacity_lower_bound/total_capacity bound.upper = capacity_upper_bound/total_capacity @@ -102,7 +102,7 @@ class PowerMetrics: Upper bound is always 0 and will be supported later. Lower bound is negative number calculated with with the formula: - ``` + ```python working_pairs: Set[BatteryData, InverterData] # working batteries from the battery pool and adjacent inverters @@ -120,7 +120,7 @@ class PowerMetrics: Lower bound is always 0 and will be supported later. Upper bound is positive number calculated with with the formula: - ``` + ```python working_pairs: Set[BatteryData, InverterData] # working batteries from the battery pool and adjacent inverters diff --git a/src/frequenz/sdk/timeseries/logical_meter/_logical_meter.py b/src/frequenz/sdk/timeseries/logical_meter/_logical_meter.py index 69e6f8f6b..be4c4d144 100644 --- a/src/frequenz/sdk/timeseries/logical_meter/_logical_meter.py +++ b/src/frequenz/sdk/timeseries/logical_meter/_logical_meter.py @@ -31,7 +31,13 @@ class LogicalMeter: normal `Receiver`s, but can also be composed to form higher-order formula streams. Example: - ``` python + ```python + from frequenz.channels import Receiver, Sender, Broadcast + from frequenz.sdk.actor import DataSourcingActor, ComponentMetricsResamplingActor + from frequenz.sdk.timeseries._resampling import ResamplerConfig + from frequenz.sdk.microgrid import connection_manager, initialize + from datetime import timedelta + channel_registry = ChannelRegistry(name="data-registry") # Create a channels for sending/receiving subscription requests @@ -53,27 +59,32 @@ class LogicalMeter: channel_registry=channel_registry, data_sourcing_request_sender=data_source_request_sender, resampling_request_receiver=resampling_request_receiver, - config=ResamplerConfig(resampling_period_s=1), + config=ResamplerConfig(resampling_period=timedelta(seconds=1)), + ) + + await initialize( + "127.0.0.1", + 50051, + ResamplerConfig(resampling_period=timedelta(seconds=1)) ) # Create a logical meter instance logical_meter = LogicalMeter( channel_registry, resampling_request_sender, - microgrid.get().component_graph, + connection_manager.get().component_graph, ) # Get a receiver for a builtin formula - grid_power_recv = logical_meter.grid_power() + grid_power_recv = logical_meter.grid_power for grid_power_sample in grid_power_recv: print(grid_power_sample) # or compose formula receivers to create a new formula net_power_recv = ( ( - logical_meter.grid_power() - - logical_meter.battery_power() - - logical_meter.pv_power() + logical_meter.grid_power + - logical_meter.pv_power ) .build("net_power") .new_receiver()