diff --git a/python/heimdall_api_client/_timestamps.py b/python/heimdall_api_client/_timestamps.py new file mode 100644 index 0000000..0118e2d --- /dev/null +++ b/python/heimdall_api_client/_timestamps.py @@ -0,0 +1,50 @@ +""" +UTC timestamp normalization for query parameters. + +The API accepts only Z-suffixed UTC timestamps: `2024-07-02T00:00:00Z` returns +200 where the equivalent `2024-07-02T00:00:00+00:00` is rejected with +`400 {"errors": {"to_timestamp": ["The value '...' is not valid."]}}`. + +The generated clients serialize every datetime query parameter with +`datetime.isoformat()`, which always emits the `+00:00` offset and never `Z`, so +no `datetime` a caller passes can produce a valid request. Rather than patch the +generated packages -- which `scripts/generate-module-client.ps1` overwrites -- +the hand-written wrappers normalize timestamps through `as_zulu` on the way in. + +Naive datetimes are assumed to be UTC; aware ones are converted, so a caller +passing a local-timezone datetime gets the window they asked for. +""" + +from __future__ import annotations + +import datetime + + +class ZuluDatetime(datetime.datetime): + """A datetime whose isoformat() renders as UTC with a `Z` suffix.""" + + def isoformat(self, sep: str = "T", timespec: str = "auto") -> str: + # Built explicitly rather than via astimezone()/replace(), which return + # this subclass and would recurse back into this method. + utc = datetime.datetime.fromtimestamp(self.timestamp(), datetime.UTC) + naive_utc = datetime.datetime(utc.year, utc.month, utc.day, utc.hour, utc.minute, utc.second, self.microsecond) + return f"{naive_utc.isoformat(sep=sep, timespec=timespec)}Z" + + +def as_zulu(timestamp: datetime.datetime) -> ZuluDatetime: + """ + Returns `timestamp` as a UTC ZuluDatetime. Naive input is treated as UTC. + """ + if timestamp.tzinfo is None: + timestamp = timestamp.replace(tzinfo=datetime.UTC) + utc = timestamp.astimezone(datetime.UTC) + return ZuluDatetime( + utc.year, + utc.month, + utc.day, + utc.hour, + utc.minute, + utc.second, + utc.microsecond, + tzinfo=datetime.UTC, + ) diff --git a/python/heimdall_api_client/capacity_monitoring.py b/python/heimdall_api_client/capacity_monitoring.py index b9960a0..5c335a5 100644 --- a/python/heimdall_api_client/capacity_monitoring.py +++ b/python/heimdall_api_client/capacity_monitoring.py @@ -1,8 +1,10 @@ from __future__ import annotations +import datetime from typing import TYPE_CHECKING from uuid import UUID +from heimdall_api_client._timestamps import as_zulu from heimdall_api_client.assets_api_client.client import AuthenticatedClient from heimdall_api_client.capacity_monitoring_api_client.api.line import ( capacity_monitoring_v1_lines_get_latest_heimdall_aar as get_latest_aar, @@ -16,15 +18,26 @@ from heimdall_api_client.capacity_monitoring_api_client.api.line import ( capacity_monitoring_v1_lines_get_latest_heimdall_dlr_forecasts as get_latest_dlr_forecasts, ) +from heimdall_api_client.capacity_monitoring_api_client.models.quantity import Quantity +from heimdall_api_client.capacity_monitoring_api_client.types import UNSET from heimdall_api_client.errors import HeimdallApiError, body_preview if TYPE_CHECKING: + from heimdall_api_client.capacity_monitoring_api_client.models.capacity_monitoring_v1_facilities_get_circuit_ratings_response_200 import ( # noqa: E501 + CapacityMonitoringV1FacilitiesGetCircuitRatingsResponse200, + ) from heimdall_api_client.capacity_monitoring_api_client.models.capacity_monitoring_v1_facilities_get_latest_circuit_rating_forecasts_response_200 import ( # noqa: E501 CapacityMonitoringV1FacilitiesGetLatestCircuitRatingForecastsResponse200, ) from heimdall_api_client.capacity_monitoring_api_client.models.capacity_monitoring_v1_facilities_get_latest_circuit_rating_response_200 import ( # noqa: E501 CapacityMonitoringV1FacilitiesGetLatestCircuitRatingResponse200, ) + from heimdall_api_client.capacity_monitoring_api_client.models.capacity_monitoring_v1_lines_get_heimdall_aars_response_200 import ( # noqa: E501 + CapacityMonitoringV1LinesGetHeimdallAarsResponse200, + ) + from heimdall_api_client.capacity_monitoring_api_client.models.capacity_monitoring_v1_lines_get_heimdall_dlrs_response_200 import ( # noqa: E501 + CapacityMonitoringV1LinesGetHeimdallDlrsResponse200, + ) from heimdall_api_client.capacity_monitoring_api_client.models.capacity_monitoring_v1_lines_get_latest_heimdall_aar_forecasts_response_200 import ( # noqa: E501 CapacityMonitoringV1LinesGetLatestHeimdallAarForecastsResponse200, ) @@ -40,9 +53,17 @@ def get_latest_heimdall_dlr( - client: AuthenticatedClient, line_id: UUID, region: str + client: AuthenticatedClient, + line_id: UUID, + region: str, + since: datetime.datetime | None = None, ) -> CapacityMonitoringV1LinesGetLatestHeimdallDlrResponse200: - response = get_latest_dlr.sync_detailed(client=client, line_id=line_id, x_region=region) + response = get_latest_dlr.sync_detailed( + client=client, + line_id=line_id, + x_region=region, + since=UNSET if since is None else as_zulu(since), + ) if response.status_code != 200: status = int(response.status_code) raise HeimdallApiError( @@ -54,9 +75,17 @@ def get_latest_heimdall_dlr( def get_latest_heimdall_aar( - client: AuthenticatedClient, line_id: UUID, region: str + client: AuthenticatedClient, + line_id: UUID, + region: str, + since: datetime.datetime | None = None, ) -> CapacityMonitoringV1LinesGetLatestHeimdallAarResponse200: - response = get_latest_aar.sync_detailed(client=client, line_id=line_id, x_region=region) + response = get_latest_aar.sync_detailed( + client=client, + line_id=line_id, + x_region=region, + since=UNSET if since is None else as_zulu(since), + ) if response.status_code != 200: status = int(response.status_code) raise HeimdallApiError( @@ -96,13 +125,21 @@ def get_latest_heimdall_arr_forecasts( def get_latest_circuit_ratring( - client: AuthenticatedClient, facility_id: UUID, x_region: str + client: AuthenticatedClient, + facility_id: UUID, + x_region: str, + since: datetime.datetime | None = None, ) -> CapacityMonitoringV1FacilitiesGetLatestCircuitRatingResponse200: from heimdall_api_client.capacity_monitoring_api_client.api.facility import ( capacity_monitoring_v1_facilities_get_latest_circuit_rating as get_latest_circuit_rating, ) - response = get_latest_circuit_rating.sync_detailed(client=client, facility_id=facility_id, x_region=x_region) + response = get_latest_circuit_rating.sync_detailed( + client=client, + facility_id=facility_id, + x_region=x_region, + since=UNSET if since is None else as_zulu(since), + ) if response.status_code != 200: status = int(response.status_code) raise HeimdallApiError( @@ -131,3 +168,103 @@ def get_latest_circuit_rating_forecasts( status_code=status, ) return response.parsed + + +def get_heimdall_dlrs( + client: AuthenticatedClient, + line_id: UUID, + region: str, + from_timestamp: datetime.datetime, + to_timestamp: datetime.datetime, + quantity: Quantity | str | None = None, +) -> CapacityMonitoringV1LinesGetHeimdallDlrsResponse200: + from heimdall_api_client.capacity_monitoring_api_client.api.line import ( + capacity_monitoring_v1_lines_get_heimdall_dlrs as _get_heimdall_dlrs, + ) + + quantity_value = UNSET + if quantity is not None: + quantity_value = quantity if isinstance(quantity, Quantity) else Quantity(quantity) + + response = _get_heimdall_dlrs.sync_detailed( + client=client, + line_id=line_id, + x_region=region, + from_timestamp=as_zulu(from_timestamp), + to_timestamp=as_zulu(to_timestamp), + quantity=quantity_value, + ) + if response.status_code != 200: + status = int(response.status_code) + raise HeimdallApiError( + f"Error fetching Heimdall DLRs: {status} {response.status_code.phrase} - {body_preview(response.content)}", + status_code=status, + ) + return response.parsed + + +def get_heimdall_aars( + client: AuthenticatedClient, + line_id: UUID, + region: str, + from_timestamp: datetime.datetime, + to_timestamp: datetime.datetime, + quantity: Quantity | str | None = None, +) -> CapacityMonitoringV1LinesGetHeimdallAarsResponse200: + from heimdall_api_client.capacity_monitoring_api_client.api.line import ( + capacity_monitoring_v1_lines_get_heimdall_aars as _get_heimdall_aars, + ) + + quantity_value = UNSET + if quantity is not None: + quantity_value = quantity if isinstance(quantity, Quantity) else Quantity(quantity) + + response = _get_heimdall_aars.sync_detailed( + client=client, + line_id=line_id, + x_region=region, + from_timestamp=as_zulu(from_timestamp), + to_timestamp=as_zulu(to_timestamp), + quantity=quantity_value, + ) + if response.status_code != 200: + status = int(response.status_code) + raise HeimdallApiError( + f"Error fetching Heimdall AARs: {status} {response.status_code.phrase} - {body_preview(response.content)}", + status_code=status, + ) + return response.parsed + + +def get_circuit_ratings( + client: AuthenticatedClient, + facility_id: UUID, + region: str, + from_timestamp: datetime.datetime, + to_timestamp: datetime.datetime, + quantity: Quantity | str | None = None, +) -> CapacityMonitoringV1FacilitiesGetCircuitRatingsResponse200: + from heimdall_api_client.capacity_monitoring_api_client.api.facility import ( + capacity_monitoring_v1_facilities_get_circuit_ratings as _get_circuit_ratings, + ) + + quantity_value = UNSET + if quantity is not None: + quantity_value = quantity if isinstance(quantity, Quantity) else Quantity(quantity) + + response = _get_circuit_ratings.sync_detailed( + client=client, + facility_id=facility_id, + x_region=region, + from_timestamp=as_zulu(from_timestamp), + to_timestamp=as_zulu(to_timestamp), + quantity=quantity_value, + ) + if response.status_code != 200: + status = int(response.status_code) + raise HeimdallApiError( + f"Error fetching circuit ratings: {status} {response.status_code.phrase}" + f" - {body_preview(response.content)}", + status_code=status, + ) + return response.parsed diff --git a/python/heimdall_api_client/capacity_monitoring_api_client/api/facility/capacity_monitoring_v1_facilities_get_latest_circuit_rating.py b/python/heimdall_api_client/capacity_monitoring_api_client/api/facility/capacity_monitoring_v1_facilities_get_latest_circuit_rating.py index 7c8ab58..a900ad8 100644 --- a/python/heimdall_api_client/capacity_monitoring_api_client/api/facility/capacity_monitoring_v1_facilities_get_latest_circuit_rating.py +++ b/python/heimdall_api_client/capacity_monitoring_api_client/api/facility/capacity_monitoring_v1_facilities_get_latest_circuit_rating.py @@ -1,3 +1,4 @@ +import datetime from http import HTTPStatus from typing import Any, cast from urllib.parse import quote @@ -22,6 +23,7 @@ def _get_kwargs( facility_id: UUID, *, quantity: Quantity | Unset = UNSET, + since: datetime.datetime | Unset = UNSET, x_region: CapacityMonitoringV1FacilitiesGetLatestCircuitRatingXRegion | Unset = CapacityMonitoringV1FacilitiesGetLatestCircuitRatingXRegion.EU, ) -> dict[str, Any]: @@ -37,6 +39,11 @@ def _get_kwargs( params["quantity"] = json_quantity + json_since: str | Unset = UNSET + if not isinstance(since, Unset): + json_since = since.isoformat() + params["since"] = json_since + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} _kwargs: dict[str, Any] = { @@ -104,6 +111,7 @@ def sync_detailed( *, client: AuthenticatedClient | Client, quantity: Quantity | Unset = UNSET, + since: datetime.datetime | Unset = UNSET, x_region: CapacityMonitoringV1FacilitiesGetLatestCircuitRatingXRegion | Unset = CapacityMonitoringV1FacilitiesGetLatestCircuitRatingXRegion.EU, ) -> Response[Any | CapacityMonitoringV1FacilitiesGetLatestCircuitRatingResponse200 | ProblemDetails]: @@ -147,6 +155,7 @@ def sync_detailed( quantity (Quantity | Unset): Which quantity to return from a rating endpoint: - `current` — value in amperes. - `apparent_power` — value converted to MVA using `S = sqrt(3) * V * I / 1,000,000`. + since (datetime.datetime | Unset): Example: 2024-07-01 12:00:00.001000+00:00. x_region (CapacityMonitoringV1FacilitiesGetLatestCircuitRatingXRegion | Unset): Default: CapacityMonitoringV1FacilitiesGetLatestCircuitRatingXRegion.EU. @@ -161,6 +170,7 @@ def sync_detailed( kwargs = _get_kwargs( facility_id=facility_id, quantity=quantity, + since=since, x_region=x_region, ) @@ -176,6 +186,7 @@ def sync( *, client: AuthenticatedClient | Client, quantity: Quantity | Unset = UNSET, + since: datetime.datetime | Unset = UNSET, x_region: CapacityMonitoringV1FacilitiesGetLatestCircuitRatingXRegion | Unset = CapacityMonitoringV1FacilitiesGetLatestCircuitRatingXRegion.EU, ) -> Any | CapacityMonitoringV1FacilitiesGetLatestCircuitRatingResponse200 | ProblemDetails | None: @@ -219,6 +230,7 @@ def sync( quantity (Quantity | Unset): Which quantity to return from a rating endpoint: - `current` — value in amperes. - `apparent_power` — value converted to MVA using `S = sqrt(3) * V * I / 1,000,000`. + since (datetime.datetime | Unset): Example: 2024-07-01 12:00:00.001000+00:00. x_region (CapacityMonitoringV1FacilitiesGetLatestCircuitRatingXRegion | Unset): Default: CapacityMonitoringV1FacilitiesGetLatestCircuitRatingXRegion.EU. @@ -234,6 +246,7 @@ def sync( facility_id=facility_id, client=client, quantity=quantity, + since=since, x_region=x_region, ).parsed @@ -243,6 +256,7 @@ async def asyncio_detailed( *, client: AuthenticatedClient | Client, quantity: Quantity | Unset = UNSET, + since: datetime.datetime | Unset = UNSET, x_region: CapacityMonitoringV1FacilitiesGetLatestCircuitRatingXRegion | Unset = CapacityMonitoringV1FacilitiesGetLatestCircuitRatingXRegion.EU, ) -> Response[Any | CapacityMonitoringV1FacilitiesGetLatestCircuitRatingResponse200 | ProblemDetails]: @@ -286,6 +300,7 @@ async def asyncio_detailed( quantity (Quantity | Unset): Which quantity to return from a rating endpoint: - `current` — value in amperes. - `apparent_power` — value converted to MVA using `S = sqrt(3) * V * I / 1,000,000`. + since (datetime.datetime | Unset): Example: 2024-07-01 12:00:00.001000+00:00. x_region (CapacityMonitoringV1FacilitiesGetLatestCircuitRatingXRegion | Unset): Default: CapacityMonitoringV1FacilitiesGetLatestCircuitRatingXRegion.EU. @@ -300,6 +315,7 @@ async def asyncio_detailed( kwargs = _get_kwargs( facility_id=facility_id, quantity=quantity, + since=since, x_region=x_region, ) @@ -313,6 +329,7 @@ async def asyncio( *, client: AuthenticatedClient | Client, quantity: Quantity | Unset = UNSET, + since: datetime.datetime | Unset = UNSET, x_region: CapacityMonitoringV1FacilitiesGetLatestCircuitRatingXRegion | Unset = CapacityMonitoringV1FacilitiesGetLatestCircuitRatingXRegion.EU, ) -> Any | CapacityMonitoringV1FacilitiesGetLatestCircuitRatingResponse200 | ProblemDetails | None: @@ -356,6 +373,7 @@ async def asyncio( quantity (Quantity | Unset): Which quantity to return from a rating endpoint: - `current` — value in amperes. - `apparent_power` — value converted to MVA using `S = sqrt(3) * V * I / 1,000,000`. + since (datetime.datetime | Unset): Example: 2024-07-01 12:00:00.001000+00:00. x_region (CapacityMonitoringV1FacilitiesGetLatestCircuitRatingXRegion | Unset): Default: CapacityMonitoringV1FacilitiesGetLatestCircuitRatingXRegion.EU. @@ -372,6 +390,7 @@ async def asyncio( facility_id=facility_id, client=client, quantity=quantity, + since=since, x_region=x_region, ) ).parsed diff --git a/python/heimdall_api_client/capacity_monitoring_api_client/api/line/capacity_monitoring_v1_lines_get_latest_heimdall_aar.py b/python/heimdall_api_client/capacity_monitoring_api_client/api/line/capacity_monitoring_v1_lines_get_latest_heimdall_aar.py index 25b663d..79a7527 100644 --- a/python/heimdall_api_client/capacity_monitoring_api_client/api/line/capacity_monitoring_v1_lines_get_latest_heimdall_aar.py +++ b/python/heimdall_api_client/capacity_monitoring_api_client/api/line/capacity_monitoring_v1_lines_get_latest_heimdall_aar.py @@ -1,3 +1,4 @@ +import datetime from http import HTTPStatus from typing import Any, cast from urllib.parse import quote @@ -22,6 +23,7 @@ def _get_kwargs( line_id: UUID, *, quantity: Quantity | Unset = UNSET, + since: datetime.datetime | Unset = UNSET, x_region: CapacityMonitoringV1LinesGetLatestHeimdallAarXRegion | Unset = CapacityMonitoringV1LinesGetLatestHeimdallAarXRegion.EU, ) -> dict[str, Any]: @@ -37,6 +39,11 @@ def _get_kwargs( params["quantity"] = json_quantity + json_since: str | Unset = UNSET + if not isinstance(since, Unset): + json_since = since.isoformat() + params["since"] = json_since + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} _kwargs: dict[str, Any] = { @@ -104,6 +111,7 @@ def sync_detailed( *, client: AuthenticatedClient | Client, quantity: Quantity | Unset = UNSET, + since: datetime.datetime | Unset = UNSET, x_region: CapacityMonitoringV1LinesGetLatestHeimdallAarXRegion | Unset = CapacityMonitoringV1LinesGetLatestHeimdallAarXRegion.EU, ) -> Response[Any | CapacityMonitoringV1LinesGetLatestHeimdallAarResponse200 | ProblemDetails]: @@ -138,6 +146,7 @@ def sync_detailed( quantity (Quantity | Unset): Which quantity to return from a rating endpoint: - `current` — value in amperes. - `apparent_power` — value converted to MVA using `S = sqrt(3) * V * I / 1,000,000`. + since (datetime.datetime | Unset): Example: 2024-07-01 12:00:00.001000+00:00. x_region (CapacityMonitoringV1LinesGetLatestHeimdallAarXRegion | Unset): Default: CapacityMonitoringV1LinesGetLatestHeimdallAarXRegion.EU. @@ -152,6 +161,7 @@ def sync_detailed( kwargs = _get_kwargs( line_id=line_id, quantity=quantity, + since=since, x_region=x_region, ) @@ -167,6 +177,7 @@ def sync( *, client: AuthenticatedClient | Client, quantity: Quantity | Unset = UNSET, + since: datetime.datetime | Unset = UNSET, x_region: CapacityMonitoringV1LinesGetLatestHeimdallAarXRegion | Unset = CapacityMonitoringV1LinesGetLatestHeimdallAarXRegion.EU, ) -> Any | CapacityMonitoringV1LinesGetLatestHeimdallAarResponse200 | ProblemDetails | None: @@ -201,6 +212,7 @@ def sync( quantity (Quantity | Unset): Which quantity to return from a rating endpoint: - `current` — value in amperes. - `apparent_power` — value converted to MVA using `S = sqrt(3) * V * I / 1,000,000`. + since (datetime.datetime | Unset): Example: 2024-07-01 12:00:00.001000+00:00. x_region (CapacityMonitoringV1LinesGetLatestHeimdallAarXRegion | Unset): Default: CapacityMonitoringV1LinesGetLatestHeimdallAarXRegion.EU. @@ -216,6 +228,7 @@ def sync( line_id=line_id, client=client, quantity=quantity, + since=since, x_region=x_region, ).parsed @@ -225,6 +238,7 @@ async def asyncio_detailed( *, client: AuthenticatedClient | Client, quantity: Quantity | Unset = UNSET, + since: datetime.datetime | Unset = UNSET, x_region: CapacityMonitoringV1LinesGetLatestHeimdallAarXRegion | Unset = CapacityMonitoringV1LinesGetLatestHeimdallAarXRegion.EU, ) -> Response[Any | CapacityMonitoringV1LinesGetLatestHeimdallAarResponse200 | ProblemDetails]: @@ -259,6 +273,7 @@ async def asyncio_detailed( quantity (Quantity | Unset): Which quantity to return from a rating endpoint: - `current` — value in amperes. - `apparent_power` — value converted to MVA using `S = sqrt(3) * V * I / 1,000,000`. + since (datetime.datetime | Unset): Example: 2024-07-01 12:00:00.001000+00:00. x_region (CapacityMonitoringV1LinesGetLatestHeimdallAarXRegion | Unset): Default: CapacityMonitoringV1LinesGetLatestHeimdallAarXRegion.EU. @@ -273,6 +288,7 @@ async def asyncio_detailed( kwargs = _get_kwargs( line_id=line_id, quantity=quantity, + since=since, x_region=x_region, ) @@ -286,6 +302,7 @@ async def asyncio( *, client: AuthenticatedClient | Client, quantity: Quantity | Unset = UNSET, + since: datetime.datetime | Unset = UNSET, x_region: CapacityMonitoringV1LinesGetLatestHeimdallAarXRegion | Unset = CapacityMonitoringV1LinesGetLatestHeimdallAarXRegion.EU, ) -> Any | CapacityMonitoringV1LinesGetLatestHeimdallAarResponse200 | ProblemDetails | None: @@ -320,6 +337,7 @@ async def asyncio( quantity (Quantity | Unset): Which quantity to return from a rating endpoint: - `current` — value in amperes. - `apparent_power` — value converted to MVA using `S = sqrt(3) * V * I / 1,000,000`. + since (datetime.datetime | Unset): Example: 2024-07-01 12:00:00.001000+00:00. x_region (CapacityMonitoringV1LinesGetLatestHeimdallAarXRegion | Unset): Default: CapacityMonitoringV1LinesGetLatestHeimdallAarXRegion.EU. @@ -336,6 +354,7 @@ async def asyncio( line_id=line_id, client=client, quantity=quantity, + since=since, x_region=x_region, ) ).parsed diff --git a/python/heimdall_api_client/capacity_monitoring_api_client/api/line/capacity_monitoring_v1_lines_get_latest_heimdall_dlr.py b/python/heimdall_api_client/capacity_monitoring_api_client/api/line/capacity_monitoring_v1_lines_get_latest_heimdall_dlr.py index e73c6cb..14c40f2 100644 --- a/python/heimdall_api_client/capacity_monitoring_api_client/api/line/capacity_monitoring_v1_lines_get_latest_heimdall_dlr.py +++ b/python/heimdall_api_client/capacity_monitoring_api_client/api/line/capacity_monitoring_v1_lines_get_latest_heimdall_dlr.py @@ -1,3 +1,4 @@ +import datetime from http import HTTPStatus from typing import Any, cast from urllib.parse import quote @@ -22,6 +23,7 @@ def _get_kwargs( line_id: UUID, *, quantity: Quantity | Unset = UNSET, + since: datetime.datetime | Unset = UNSET, x_region: CapacityMonitoringV1LinesGetLatestHeimdallDlrXRegion | Unset = CapacityMonitoringV1LinesGetLatestHeimdallDlrXRegion.EU, ) -> dict[str, Any]: @@ -37,6 +39,11 @@ def _get_kwargs( params["quantity"] = json_quantity + json_since: str | Unset = UNSET + if not isinstance(since, Unset): + json_since = since.isoformat() + params["since"] = json_since + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} _kwargs: dict[str, Any] = { @@ -104,6 +111,7 @@ def sync_detailed( *, client: AuthenticatedClient | Client, quantity: Quantity | Unset = UNSET, + since: datetime.datetime | Unset = UNSET, x_region: CapacityMonitoringV1LinesGetLatestHeimdallDlrXRegion | Unset = CapacityMonitoringV1LinesGetLatestHeimdallDlrXRegion.EU, ) -> Response[Any | CapacityMonitoringV1LinesGetLatestHeimdallDlrResponse200 | ProblemDetails]: @@ -137,6 +145,7 @@ def sync_detailed( quantity (Quantity | Unset): Which quantity to return from a rating endpoint: - `current` — value in amperes. - `apparent_power` — value converted to MVA using `S = sqrt(3) * V * I / 1,000,000`. + since (datetime.datetime | Unset): Example: 2024-07-01 12:00:00.001000+00:00. x_region (CapacityMonitoringV1LinesGetLatestHeimdallDlrXRegion | Unset): Default: CapacityMonitoringV1LinesGetLatestHeimdallDlrXRegion.EU. @@ -151,6 +160,7 @@ def sync_detailed( kwargs = _get_kwargs( line_id=line_id, quantity=quantity, + since=since, x_region=x_region, ) @@ -166,6 +176,7 @@ def sync( *, client: AuthenticatedClient | Client, quantity: Quantity | Unset = UNSET, + since: datetime.datetime | Unset = UNSET, x_region: CapacityMonitoringV1LinesGetLatestHeimdallDlrXRegion | Unset = CapacityMonitoringV1LinesGetLatestHeimdallDlrXRegion.EU, ) -> Any | CapacityMonitoringV1LinesGetLatestHeimdallDlrResponse200 | ProblemDetails | None: @@ -199,6 +210,7 @@ def sync( quantity (Quantity | Unset): Which quantity to return from a rating endpoint: - `current` — value in amperes. - `apparent_power` — value converted to MVA using `S = sqrt(3) * V * I / 1,000,000`. + since (datetime.datetime | Unset): Example: 2024-07-01 12:00:00.001000+00:00. x_region (CapacityMonitoringV1LinesGetLatestHeimdallDlrXRegion | Unset): Default: CapacityMonitoringV1LinesGetLatestHeimdallDlrXRegion.EU. @@ -214,6 +226,7 @@ def sync( line_id=line_id, client=client, quantity=quantity, + since=since, x_region=x_region, ).parsed @@ -223,6 +236,7 @@ async def asyncio_detailed( *, client: AuthenticatedClient | Client, quantity: Quantity | Unset = UNSET, + since: datetime.datetime | Unset = UNSET, x_region: CapacityMonitoringV1LinesGetLatestHeimdallDlrXRegion | Unset = CapacityMonitoringV1LinesGetLatestHeimdallDlrXRegion.EU, ) -> Response[Any | CapacityMonitoringV1LinesGetLatestHeimdallDlrResponse200 | ProblemDetails]: @@ -256,6 +270,7 @@ async def asyncio_detailed( quantity (Quantity | Unset): Which quantity to return from a rating endpoint: - `current` — value in amperes. - `apparent_power` — value converted to MVA using `S = sqrt(3) * V * I / 1,000,000`. + since (datetime.datetime | Unset): Example: 2024-07-01 12:00:00.001000+00:00. x_region (CapacityMonitoringV1LinesGetLatestHeimdallDlrXRegion | Unset): Default: CapacityMonitoringV1LinesGetLatestHeimdallDlrXRegion.EU. @@ -270,6 +285,7 @@ async def asyncio_detailed( kwargs = _get_kwargs( line_id=line_id, quantity=quantity, + since=since, x_region=x_region, ) @@ -283,6 +299,7 @@ async def asyncio( *, client: AuthenticatedClient | Client, quantity: Quantity | Unset = UNSET, + since: datetime.datetime | Unset = UNSET, x_region: CapacityMonitoringV1LinesGetLatestHeimdallDlrXRegion | Unset = CapacityMonitoringV1LinesGetLatestHeimdallDlrXRegion.EU, ) -> Any | CapacityMonitoringV1LinesGetLatestHeimdallDlrResponse200 | ProblemDetails | None: @@ -316,6 +333,7 @@ async def asyncio( quantity (Quantity | Unset): Which quantity to return from a rating endpoint: - `current` — value in amperes. - `apparent_power` — value converted to MVA using `S = sqrt(3) * V * I / 1,000,000`. + since (datetime.datetime | Unset): Example: 2024-07-01 12:00:00.001000+00:00. x_region (CapacityMonitoringV1LinesGetLatestHeimdallDlrXRegion | Unset): Default: CapacityMonitoringV1LinesGetLatestHeimdallDlrXRegion.EU. @@ -332,6 +350,7 @@ async def asyncio( line_id=line_id, client=client, quantity=quantity, + since=since, x_region=x_region, ) ).parsed diff --git a/python/heimdall_api_client/client.py b/python/heimdall_api_client/client.py index 0f0adf2..365b9c6 100644 --- a/python/heimdall_api_client/client.py +++ b/python/heimdall_api_client/client.py @@ -14,11 +14,15 @@ from heimdall_api_client.assets_api_client.client import AuthenticatedClient from heimdall_api_client.auth import AuthService from heimdall_api_client.capacity_monitoring import ( + get_circuit_ratings, + get_heimdall_aars, + get_heimdall_dlrs, get_latest_heimdall_aar, get_latest_heimdall_arr_forecasts, get_latest_heimdall_dlr, get_latest_heimdall_dlr_forecasts, ) +from heimdall_api_client.capacity_monitoring_api_client.models.quantity import Quantity from heimdall_api_client.errors import HeimdallApiError from heimdall_api_client.grid_insights_api_client.models.unit_system import UnitSystem @@ -28,12 +32,21 @@ from heimdall_api_client.assets_api_client.models.assets_v1_get_assets_response_200 import ( AssetsV1GetAssetsResponse200, ) + from heimdall_api_client.capacity_monitoring_api_client.models.capacity_monitoring_v1_facilities_get_circuit_ratings_response_200 import ( # noqa: E501 + CapacityMonitoringV1FacilitiesGetCircuitRatingsResponse200, + ) from heimdall_api_client.capacity_monitoring_api_client.models.capacity_monitoring_v1_facilities_get_latest_circuit_rating_forecasts_response_200 import ( # noqa: E501 CapacityMonitoringV1FacilitiesGetLatestCircuitRatingForecastsResponse200, ) from heimdall_api_client.capacity_monitoring_api_client.models.capacity_monitoring_v1_facilities_get_latest_circuit_rating_response_200 import ( # noqa: E501 CapacityMonitoringV1FacilitiesGetLatestCircuitRatingResponse200, ) + from heimdall_api_client.capacity_monitoring_api_client.models.capacity_monitoring_v1_lines_get_heimdall_aars_response_200 import ( # noqa: E501 + CapacityMonitoringV1LinesGetHeimdallAarsResponse200, + ) + from heimdall_api_client.capacity_monitoring_api_client.models.capacity_monitoring_v1_lines_get_heimdall_dlrs_response_200 import ( # noqa: E501 + CapacityMonitoringV1LinesGetHeimdallDlrsResponse200, + ) from heimdall_api_client.capacity_monitoring_api_client.models.capacity_monitoring_v1_lines_get_latest_heimdall_aar_forecasts_response_200 import ( # noqa: E501 CapacityMonitoringV1LinesGetLatestHeimdallAarForecastsResponse200, ) @@ -46,6 +59,24 @@ from heimdall_api_client.capacity_monitoring_api_client.models.capacity_monitoring_v1_lines_get_latest_heimdall_dlr_response_200 import ( # noqa: E501 CapacityMonitoringV1LinesGetLatestHeimdallDlrResponse200, ) + from heimdall_api_client.grid_insights_api_client.models.grid_insights_v1_lines_get_apparent_power_response_200 import ( # noqa: E501 + GridInsightsV1LinesGetApparentPowerResponse200, + ) + from heimdall_api_client.grid_insights_api_client.models.grid_insights_v1_lines_get_conductor_temperatures_response_200 import ( # noqa: E501 + GridInsightsV1LinesGetConductorTemperaturesResponse200, + ) + from heimdall_api_client.grid_insights_api_client.models.grid_insights_v1_lines_get_currents_response_200 import ( # noqa: E501 + GridInsightsV1LinesGetCurrentsResponse200, + ) + from heimdall_api_client.grid_insights_api_client.models.grid_insights_v1_lines_get_icing_forecast_response_200 import ( # noqa: E501 + GridInsightsV1LinesGetIcingForecastResponse200, + ) + from heimdall_api_client.grid_insights_api_client.models.grid_insights_v1_lines_get_icing_response_200 import ( # noqa: E501 + GridInsightsV1LinesGetIcingResponse200, + ) + from heimdall_api_client.grid_insights_api_client.models.grid_insights_v1_lines_get_latest_apparent_power_response_200 import ( # noqa: E501 + GridInsightsV1LinesGetLatestApparentPowerResponse200, + ) from heimdall_api_client.grid_insights_api_client.models.grid_insights_v1_lines_get_latest_conductor_temperature_response_200 import ( # noqa: E501 GridInsightsV1LinesGetLatestConductorTemperatureResponse200, ) @@ -55,6 +86,9 @@ from heimdall_api_client.grid_insights_api_client.models.grid_insights_v1_lines_get_latest_icing_response_200 import ( # noqa: E501 GridInsightsV1LinesGetLatestIcingResponse200, ) + from heimdall_api_client.grid_insights_api_client.models.grid_insights_v1_lines_get_sag_and_clearance_response_200 import ( # noqa: E501 + GridInsightsV1LinesGetSagAndClearanceResponse200, + ) _MAX_RETRY_ATTEMPTS = 3 @@ -203,23 +237,37 @@ def get_assets(self) -> AssetsV1GetAssetsResponse200: lambda: get_assets(client=self._get_authenticated_client(), x_region=self._get_region()) ) - def get_latest_heimdall_dlr(self, line_id: UUID) -> CapacityMonitoringV1LinesGetLatestHeimdallDlrResponse200: + def get_latest_heimdall_dlr( + self, line_id: UUID, since: datetime.datetime | None = None + ) -> CapacityMonitoringV1LinesGetLatestHeimdallDlrResponse200: """ Returns the latest Heimdall DLR (Dynamic Line rating) data. + + `since` bounds how old the returned value may be. """ return self._execute_with_retry( lambda: get_latest_heimdall_dlr( - client=self._get_authenticated_client(), line_id=line_id, region=self._get_region() + client=self._get_authenticated_client(), + line_id=line_id, + region=self._get_region(), + since=since, ) ) - def get_latest_heimdall_aar(self, line_id: UUID) -> CapacityMonitoringV1LinesGetLatestHeimdallAarResponse200: + def get_latest_heimdall_aar( + self, line_id: UUID, since: datetime.datetime | None = None + ) -> CapacityMonitoringV1LinesGetLatestHeimdallAarResponse200: """ Returns the latest Heimdall AAR (Available Ampacity Rating) data. + + `since` bounds how old the returned value may be. """ return self._execute_with_retry( lambda: get_latest_heimdall_aar( - client=self._get_authenticated_client(), line_id=line_id, region=self._get_region() + client=self._get_authenticated_client(), + line_id=line_id, + region=self._get_region(), + since=since, ) ) @@ -248,16 +296,21 @@ def get_latest_heimdall_aar_forecasts( ) def get_latest_circuit_rating( - self, facility_id: UUID + self, facility_id: UUID, since: datetime.datetime | None = None ) -> CapacityMonitoringV1FacilitiesGetLatestCircuitRatingResponse200: """ Returns the latest circuit rating for a given facility. + + `since` bounds how old the returned value may be. """ from heimdall_api_client.capacity_monitoring import get_latest_circuit_ratring return self._execute_with_retry( lambda: get_latest_circuit_ratring( - client=self._get_authenticated_client(), facility_id=facility_id, x_region=self._get_region() + client=self._get_authenticated_client(), + facility_id=facility_id, + x_region=self._get_region(), + since=since, ) ) @@ -276,28 +329,40 @@ def get_latest_circuit_rating_forecasts( ) def get_latest_conductor_temperature( - self, line_id: UUID + self, line_id: UUID, since: datetime.datetime | None = None ) -> GridInsightsV1LinesGetLatestConductorTemperatureResponse200: """ Returns the latest conductor temperature for a given line. + + `since` bounds how old the returned measurement may be. """ from heimdall_api_client.grid_insights import get_latest_conductor_temperature return self._execute_with_retry( lambda: get_latest_conductor_temperature( - client=self._get_authenticated_client(), line_id=line_id, region=self._get_region() + client=self._get_authenticated_client(), + line_id=line_id, + region=self._get_region(), + since=since, ) ) - def get_latest_current(self, line_id: UUID) -> GridInsightsV1LinesGetLatestCurrentResponse200: + def get_latest_current( + self, line_id: UUID, since: datetime.datetime | None = None + ) -> GridInsightsV1LinesGetLatestCurrentResponse200: """ Returns the latest current for a given line. + + `since` bounds how old the returned measurement may be. """ from heimdall_api_client.grid_insights import get_latest_current return self._execute_with_retry( lambda: get_latest_current( - client=self._get_authenticated_client(), line_id=line_id, region=self._get_region() + client=self._get_authenticated_client(), + line_id=line_id, + region=self._get_region(), + since=since, ) ) @@ -342,3 +407,215 @@ def get_latest_sag_and_clearance( since=since, ) ) + + def get_currents( + self, + line_id: UUID, + from_timestamp: datetime.datetime, + to_timestamp: datetime.datetime, + ) -> GridInsightsV1LinesGetCurrentsResponse200: + """ + Returns historical current measurements for a given line. + """ + from heimdall_api_client.grid_insights import get_currents + + return self._execute_with_retry( + lambda: get_currents( + client=self._get_authenticated_client(), + line_id=line_id, + region=self._get_region(), + from_timestamp=from_timestamp, + to_timestamp=to_timestamp, + ) + ) + + def get_conductor_temperatures( + self, + line_id: UUID, + from_timestamp: datetime.datetime, + to_timestamp: datetime.datetime, + unit_system: UnitSystem | str | None = None, + ) -> GridInsightsV1LinesGetConductorTemperaturesResponse200: + """ + Returns historical conductor temperature measurements for a given line. + """ + from heimdall_api_client.grid_insights import get_conductor_temperatures + + return self._execute_with_retry( + lambda: get_conductor_temperatures( + client=self._get_authenticated_client(), + line_id=line_id, + region=self._get_region(), + from_timestamp=from_timestamp, + to_timestamp=to_timestamp, + unit_system=unit_system, + ) + ) + + def get_heimdall_dlrs( + self, + line_id: UUID, + from_timestamp: datetime.datetime, + to_timestamp: datetime.datetime, + quantity: Quantity | str | None = None, + ) -> CapacityMonitoringV1LinesGetHeimdallDlrsResponse200: + """ + Returns historical Heimdall DLR (Dynamic Line Rating) values for a given line. + """ + return self._execute_with_retry( + lambda: get_heimdall_dlrs( + client=self._get_authenticated_client(), + line_id=line_id, + region=self._get_region(), + from_timestamp=from_timestamp, + to_timestamp=to_timestamp, + quantity=quantity, + ) + ) + + def get_heimdall_aars( + self, + line_id: UUID, + from_timestamp: datetime.datetime, + to_timestamp: datetime.datetime, + quantity: Quantity | str | None = None, + ) -> CapacityMonitoringV1LinesGetHeimdallAarsResponse200: + """ + Returns historical Heimdall AAR (Available Ampacity Rating) values for a given line. + """ + return self._execute_with_retry( + lambda: get_heimdall_aars( + client=self._get_authenticated_client(), + line_id=line_id, + region=self._get_region(), + from_timestamp=from_timestamp, + to_timestamp=to_timestamp, + quantity=quantity, + ) + ) + + def get_circuit_ratings( + self, + facility_id: UUID, + from_timestamp: datetime.datetime, + to_timestamp: datetime.datetime, + quantity: Quantity | str | None = None, + ) -> CapacityMonitoringV1FacilitiesGetCircuitRatingsResponse200: + """ + Returns historical circuit ratings for a given facility. + """ + return self._execute_with_retry( + lambda: get_circuit_ratings( + client=self._get_authenticated_client(), + facility_id=facility_id, + region=self._get_region(), + from_timestamp=from_timestamp, + to_timestamp=to_timestamp, + quantity=quantity, + ) + ) + + def get_icing( + self, + line_id: UUID, + from_timestamp: datetime.datetime, + to_timestamp: datetime.datetime, + unit_system: UnitSystem | str | None = None, + ) -> GridInsightsV1LinesGetIcingResponse200: + """ + Returns historical icing measurements for a given line. + """ + from heimdall_api_client.grid_insights import get_icing + + return self._execute_with_retry( + lambda: get_icing( + client=self._get_authenticated_client(), + line_id=line_id, + region=self._get_region(), + from_timestamp=from_timestamp, + to_timestamp=to_timestamp, + unit_system=unit_system, + ) + ) + + def get_sag_and_clearance( + self, + line_id: UUID, + from_timestamp: datetime.datetime, + to_timestamp: datetime.datetime, + unit_system: UnitSystem | str | None = None, + ) -> GridInsightsV1LinesGetSagAndClearanceResponse200: + """ + Returns historical sag and clearance measurements for a given line. + """ + from heimdall_api_client.grid_insights import get_sag_and_clearance + + return self._execute_with_retry( + lambda: get_sag_and_clearance( + client=self._get_authenticated_client(), + line_id=line_id, + region=self._get_region(), + from_timestamp=from_timestamp, + to_timestamp=to_timestamp, + unit_system=unit_system, + ) + ) + + def get_apparent_power( + self, + line_id: UUID, + from_timestamp: datetime.datetime, + to_timestamp: datetime.datetime, + ) -> GridInsightsV1LinesGetApparentPowerResponse200: + """ + Returns historical apparent power measurements for a given line. + """ + from heimdall_api_client.grid_insights import get_apparent_power + + return self._execute_with_retry( + lambda: get_apparent_power( + client=self._get_authenticated_client(), + line_id=line_id, + region=self._get_region(), + from_timestamp=from_timestamp, + to_timestamp=to_timestamp, + ) + ) + + def get_latest_apparent_power( + self, line_id: UUID, since: datetime.datetime | None = None + ) -> GridInsightsV1LinesGetLatestApparentPowerResponse200: + """ + Returns the latest apparent power measurement for a given line. + + `since` bounds how old the returned measurement may be. + """ + from heimdall_api_client.grid_insights import get_latest_apparent_power + + return self._execute_with_retry( + lambda: get_latest_apparent_power( + client=self._get_authenticated_client(), + line_id=line_id, + region=self._get_region(), + since=since, + ) + ) + + def get_icing_forecast( + self, + line_id: UUID, + unit_system: UnitSystem | str | None = None, + ) -> GridInsightsV1LinesGetIcingForecastResponse200: + """ + Returns the latest icing forecast for a given line. + """ + from heimdall_api_client.grid_insights import get_icing_forecast + + return self._execute_with_retry( + lambda: get_icing_forecast( + client=self._get_authenticated_client(), + line_id=line_id, + region=self._get_region(), + unit_system=unit_system, + ) + ) diff --git a/python/heimdall_api_client/grid_insights.py b/python/heimdall_api_client/grid_insights.py index e27b193..ceb5dfb 100644 --- a/python/heimdall_api_client/grid_insights.py +++ b/python/heimdall_api_client/grid_insights.py @@ -4,12 +4,31 @@ from typing import TYPE_CHECKING from uuid import UUID +from heimdall_api_client._timestamps import as_zulu from heimdall_api_client.assets_api_client.client import AuthenticatedClient from heimdall_api_client.errors import HeimdallApiError, body_preview from heimdall_api_client.grid_insights_api_client.models.unit_system import UnitSystem from heimdall_api_client.grid_insights_api_client.types import UNSET if TYPE_CHECKING: + from heimdall_api_client.grid_insights_api_client.models.grid_insights_v1_lines_get_apparent_power_response_200 import ( # noqa: E501 + GridInsightsV1LinesGetApparentPowerResponse200, + ) + from heimdall_api_client.grid_insights_api_client.models.grid_insights_v1_lines_get_conductor_temperatures_response_200 import ( # noqa: E501 + GridInsightsV1LinesGetConductorTemperaturesResponse200, + ) + from heimdall_api_client.grid_insights_api_client.models.grid_insights_v1_lines_get_currents_response_200 import ( # noqa: E501 + GridInsightsV1LinesGetCurrentsResponse200, + ) + from heimdall_api_client.grid_insights_api_client.models.grid_insights_v1_lines_get_icing_forecast_response_200 import ( # noqa: E501 + GridInsightsV1LinesGetIcingForecastResponse200, + ) + from heimdall_api_client.grid_insights_api_client.models.grid_insights_v1_lines_get_icing_response_200 import ( # noqa: E501 + GridInsightsV1LinesGetIcingResponse200, + ) + from heimdall_api_client.grid_insights_api_client.models.grid_insights_v1_lines_get_latest_apparent_power_response_200 import ( # noqa: E501 + GridInsightsV1LinesGetLatestApparentPowerResponse200, + ) from heimdall_api_client.grid_insights_api_client.models.grid_insights_v1_lines_get_latest_conductor_temperature_response_200 import ( # noqa: E501 GridInsightsV1LinesGetLatestConductorTemperatureResponse200, ) @@ -19,16 +38,27 @@ from heimdall_api_client.grid_insights_api_client.models.grid_insights_v1_lines_get_latest_icing_response_200 import ( # noqa: E501 GridInsightsV1LinesGetLatestIcingResponse200, ) + from heimdall_api_client.grid_insights_api_client.models.grid_insights_v1_lines_get_sag_and_clearance_response_200 import ( # noqa: E501 + GridInsightsV1LinesGetSagAndClearanceResponse200, + ) def get_latest_conductor_temperature( - client: AuthenticatedClient, line_id: UUID, region: str + client: AuthenticatedClient, + line_id: UUID, + region: str, + since: datetime.datetime | None = None, ) -> GridInsightsV1LinesGetLatestConductorTemperatureResponse200: from heimdall_api_client.grid_insights_api_client.api.line import ( grid_insights_v1_lines_get_latest_conductor_temperature as get_latest_conductor_temperature, ) - response = get_latest_conductor_temperature.sync_detailed(client=client, line_id=line_id, x_region=region) + response = get_latest_conductor_temperature.sync_detailed( + client=client, + line_id=line_id, + x_region=region, + since=UNSET if since is None else as_zulu(since), + ) if response.status_code != 200: status = int(response.status_code) raise HeimdallApiError( @@ -40,13 +70,21 @@ def get_latest_conductor_temperature( def get_latest_current( - client: AuthenticatedClient, line_id: UUID, region: str + client: AuthenticatedClient, + line_id: UUID, + region: str, + since: datetime.datetime | None = None, ) -> GridInsightsV1LinesGetLatestCurrentResponse200: from heimdall_api_client.grid_insights_api_client.api.line import ( grid_insights_v1_lines_get_latest_current as get_latest_current, ) - response = get_latest_current.sync_detailed(client=client, line_id=line_id, x_region=region) + response = get_latest_current.sync_detailed( + client=client, + line_id=line_id, + x_region=region, + since=UNSET if since is None else as_zulu(since), + ) if response.status_code != 200: status = int(response.status_code) raise HeimdallApiError( @@ -71,7 +109,7 @@ def get_latest_icing( if unit_system is not None: unit_system_value = unit_system if isinstance(unit_system, UnitSystem) else UnitSystem(unit_system) - since_value = UNSET if since is None else since + since_value = UNSET if since is None else as_zulu(since) response = get_latest_icing.sync_detailed( client=client, @@ -104,7 +142,7 @@ def get_latest_sag_and_clearance( if unit_system is not None: unit_system_value = unit_system if isinstance(unit_system, UnitSystem) else UnitSystem(unit_system) - since_value = UNSET if since is None else since + since_value = UNSET if since is None else as_zulu(since) response = get_latest_sag_and_clearance.sync_detailed( client=client, @@ -121,3 +159,213 @@ def get_latest_sag_and_clearance( status_code=status, ) return response.parsed + + +def get_icing( + client: AuthenticatedClient, + line_id: UUID, + region: str, + from_timestamp: datetime.datetime, + to_timestamp: datetime.datetime, + unit_system: UnitSystem | str | None = None, +) -> GridInsightsV1LinesGetIcingResponse200: + from heimdall_api_client.grid_insights_api_client.api.line import ( + grid_insights_v1_lines_get_icing as _get_icing, + ) + + unit_system_value = UNSET + if unit_system is not None: + unit_system_value = unit_system if isinstance(unit_system, UnitSystem) else UnitSystem(unit_system) + + response = _get_icing.sync_detailed( + client=client, + line_id=line_id, + x_region=region, + from_timestamp=as_zulu(from_timestamp), + to_timestamp=as_zulu(to_timestamp), + unit_system=unit_system_value, + ) + if response.status_code != 200: + status = int(response.status_code) + raise HeimdallApiError( + f"Error fetching icing: {status} {response.status_code.phrase} - {body_preview(response.content)}", + status_code=status, + ) + return response.parsed + + +def get_sag_and_clearance( + client: AuthenticatedClient, + line_id: UUID, + region: str, + from_timestamp: datetime.datetime, + to_timestamp: datetime.datetime, + unit_system: UnitSystem | str | None = None, +) -> GridInsightsV1LinesGetSagAndClearanceResponse200: + from heimdall_api_client.grid_insights_api_client.api.line import ( + grid_insights_v1_lines_get_sag_and_clearance as _get_sag_and_clearance, + ) + + unit_system_value = UNSET + if unit_system is not None: + unit_system_value = unit_system if isinstance(unit_system, UnitSystem) else UnitSystem(unit_system) + + response = _get_sag_and_clearance.sync_detailed( + client=client, + line_id=line_id, + x_region=region, + from_timestamp=as_zulu(from_timestamp), + to_timestamp=as_zulu(to_timestamp), + unit_system=unit_system_value, + ) + if response.status_code != 200: + status = int(response.status_code) + raise HeimdallApiError( + f"Error fetching sag and clearance: {status} {response.status_code.phrase}" + f" - {body_preview(response.content)}", + status_code=status, + ) + return response.parsed + + +def get_apparent_power( + client: AuthenticatedClient, + line_id: UUID, + region: str, + from_timestamp: datetime.datetime, + to_timestamp: datetime.datetime, +) -> GridInsightsV1LinesGetApparentPowerResponse200: + from heimdall_api_client.grid_insights_api_client.api.line import ( + grid_insights_v1_lines_get_apparent_power as _get_apparent_power, + ) + + response = _get_apparent_power.sync_detailed( + client=client, + line_id=line_id, + x_region=region, + from_timestamp=as_zulu(from_timestamp), + to_timestamp=as_zulu(to_timestamp), + ) + if response.status_code != 200: + status = int(response.status_code) + raise HeimdallApiError( + f"Error fetching apparent power: {status} {response.status_code.phrase} - {body_preview(response.content)}", + status_code=status, + ) + return response.parsed + + +def get_currents( + client: AuthenticatedClient, + line_id: UUID, + region: str, + from_timestamp: datetime.datetime, + to_timestamp: datetime.datetime, +) -> GridInsightsV1LinesGetCurrentsResponse200: + from heimdall_api_client.grid_insights_api_client.api.line import ( + grid_insights_v1_lines_get_currents as _get_currents, + ) + + response = _get_currents.sync_detailed( + client=client, + line_id=line_id, + x_region=region, + from_timestamp=as_zulu(from_timestamp), + to_timestamp=as_zulu(to_timestamp), + ) + if response.status_code != 200: + status = int(response.status_code) + raise HeimdallApiError( + f"Error fetching currents: {status} {response.status_code.phrase} - {body_preview(response.content)}", + status_code=status, + ) + return response.parsed + + +def get_latest_apparent_power( + client: AuthenticatedClient, + line_id: UUID, + region: str, + since: datetime.datetime | None = None, +) -> GridInsightsV1LinesGetLatestApparentPowerResponse200: + from heimdall_api_client.grid_insights_api_client.api.line import ( + grid_insights_v1_lines_get_latest_apparent_power as _get_latest_apparent_power, + ) + + response = _get_latest_apparent_power.sync_detailed( + client=client, + line_id=line_id, + x_region=region, + since=UNSET if since is None else as_zulu(since), + ) + if response.status_code != 200: + status = int(response.status_code) + raise HeimdallApiError( + f"Error fetching latest apparent power: {status} {response.status_code.phrase}" + f" - {body_preview(response.content)}", + status_code=status, + ) + return response.parsed + + +def get_icing_forecast( + client: AuthenticatedClient, + line_id: UUID, + region: str, + unit_system: UnitSystem | str | None = None, +) -> GridInsightsV1LinesGetIcingForecastResponse200: + from heimdall_api_client.grid_insights_api_client.api.line import ( + grid_insights_v1_lines_get_icing_forecast as _get_icing_forecast, + ) + + unit_system_value = UNSET + if unit_system is not None: + unit_system_value = unit_system if isinstance(unit_system, UnitSystem) else UnitSystem(unit_system) + + response = _get_icing_forecast.sync_detailed( + client=client, + line_id=line_id, + x_region=region, + unit_system=unit_system_value, + ) + if response.status_code != 200: + status = int(response.status_code) + raise HeimdallApiError( + f"Error fetching icing forecast: {status} {response.status_code.phrase} - {body_preview(response.content)}", + status_code=status, + ) + return response.parsed + + +def get_conductor_temperatures( + client: AuthenticatedClient, + line_id: UUID, + region: str, + from_timestamp: datetime.datetime, + to_timestamp: datetime.datetime, + unit_system: UnitSystem | str | None = None, +) -> GridInsightsV1LinesGetConductorTemperaturesResponse200: + from heimdall_api_client.grid_insights_api_client.api.line import ( + grid_insights_v1_lines_get_conductor_temperatures as _get_conductor_temperatures, + ) + + unit_system_value = UNSET + if unit_system is not None: + unit_system_value = unit_system if isinstance(unit_system, UnitSystem) else UnitSystem(unit_system) + + response = _get_conductor_temperatures.sync_detailed( + client=client, + line_id=line_id, + x_region=region, + from_timestamp=as_zulu(from_timestamp), + to_timestamp=as_zulu(to_timestamp), + unit_system=unit_system_value, + ) + if response.status_code != 200: + status = int(response.status_code) + raise HeimdallApiError( + f"Error fetching conductor temperatures: {status} {response.status_code.phrase}" + f" - {body_preview(response.content)}", + status_code=status, + ) + return response.parsed diff --git a/python/heimdall_api_client/grid_insights_api_client/api/line/grid_insights_v1_lines_get_icing.py b/python/heimdall_api_client/grid_insights_api_client/api/line/grid_insights_v1_lines_get_icing.py index 0f16a37..d33b49c 100644 --- a/python/heimdall_api_client/grid_insights_api_client/api/line/grid_insights_v1_lines_get_icing.py +++ b/python/heimdall_api_client/grid_insights_api_client/api/line/grid_insights_v1_lines_get_icing.py @@ -128,7 +128,8 @@ def sync_detailed( - **`ice_weight`**: The mass of ice accumulated on the conductor. - **`tension`**: The mechanical tension force in the conductor, which increases as ice accumulates. - **`tension_percentage_of_break_strength`**: Safety-critical metric showing how close the conductor - is to its breaking point. + is to its breaking point or how close it is to the maximum tension it's designed to safely + withstand. - **`timestamp`**: Time (UTC) when the icing measurements were calculated for the span phase. Timestamps may differ per conductor due to data availability. @@ -193,7 +194,8 @@ def sync( - **`ice_weight`**: The mass of ice accumulated on the conductor. - **`tension`**: The mechanical tension force in the conductor, which increases as ice accumulates. - **`tension_percentage_of_break_strength`**: Safety-critical metric showing how close the conductor - is to its breaking point. + is to its breaking point or how close it is to the maximum tension it's designed to safely + withstand. - **`timestamp`**: Time (UTC) when the icing measurements were calculated for the span phase. Timestamps may differ per conductor due to data availability. @@ -253,7 +255,8 @@ async def asyncio_detailed( - **`ice_weight`**: The mass of ice accumulated on the conductor. - **`tension`**: The mechanical tension force in the conductor, which increases as ice accumulates. - **`tension_percentage_of_break_strength`**: Safety-critical metric showing how close the conductor - is to its breaking point. + is to its breaking point or how close it is to the maximum tension it's designed to safely + withstand. - **`timestamp`**: Time (UTC) when the icing measurements were calculated for the span phase. Timestamps may differ per conductor due to data availability. @@ -316,7 +319,8 @@ async def asyncio( - **`ice_weight`**: The mass of ice accumulated on the conductor. - **`tension`**: The mechanical tension force in the conductor, which increases as ice accumulates. - **`tension_percentage_of_break_strength`**: Safety-critical metric showing how close the conductor - is to its breaking point. + is to its breaking point or how close it is to the maximum tension it's designed to safely + withstand. - **`timestamp`**: Time (UTC) when the icing measurements were calculated for the span phase. Timestamps may differ per conductor due to data availability. diff --git a/python/heimdall_api_client/grid_insights_api_client/api/line/grid_insights_v1_lines_get_latest_apparent_power.py b/python/heimdall_api_client/grid_insights_api_client/api/line/grid_insights_v1_lines_get_latest_apparent_power.py index 4361bbb..69ba4a6 100644 --- a/python/heimdall_api_client/grid_insights_api_client/api/line/grid_insights_v1_lines_get_latest_apparent_power.py +++ b/python/heimdall_api_client/grid_insights_api_client/api/line/grid_insights_v1_lines_get_latest_apparent_power.py @@ -1,3 +1,4 @@ +import datetime from http import HTTPStatus from typing import Any, cast from urllib.parse import quote @@ -14,12 +15,13 @@ GridInsightsV1LinesGetLatestApparentPowerXRegion, ) from ...models.problem_details import ProblemDetails -from ...types import Response, Unset +from ...types import UNSET, Response, Unset def _get_kwargs( line_id: UUID, *, + since: datetime.datetime | Unset = UNSET, x_region: GridInsightsV1LinesGetLatestApparentPowerXRegion | Unset = GridInsightsV1LinesGetLatestApparentPowerXRegion.EU, ) -> dict[str, Any]: @@ -27,11 +29,21 @@ def _get_kwargs( if not isinstance(x_region, Unset): headers["x-region"] = str(x_region) + params: dict[str, Any] = {} + + json_since: str | Unset = UNSET + if not isinstance(since, Unset): + json_since = since.isoformat() + params["since"] = json_since + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + _kwargs: dict[str, Any] = { "method": "get", "url": "/grid_insights/v1/lines/{line_id}/apparent_power/latest".format( line_id=quote(str(line_id), safe=""), ), + "params": params, } _kwargs["headers"] = headers @@ -90,6 +102,7 @@ def sync_detailed( line_id: UUID, *, client: AuthenticatedClient | Client, + since: datetime.datetime | Unset = UNSET, x_region: GridInsightsV1LinesGetLatestApparentPowerXRegion | Unset = GridInsightsV1LinesGetLatestApparentPowerXRegion.EU, ) -> Response[Any | GridInsightsV1LinesGetLatestApparentPowerResponse200 | ProblemDetails]: @@ -105,6 +118,7 @@ def sync_detailed( Args: line_id (UUID): + since (datetime.datetime | Unset): Example: 2024-07-01 12:00:00.001000+00:00. x_region (GridInsightsV1LinesGetLatestApparentPowerXRegion | Unset): Default: GridInsightsV1LinesGetLatestApparentPowerXRegion.EU. @@ -118,6 +132,7 @@ def sync_detailed( kwargs = _get_kwargs( line_id=line_id, + since=since, x_region=x_region, ) @@ -132,6 +147,7 @@ def sync( line_id: UUID, *, client: AuthenticatedClient | Client, + since: datetime.datetime | Unset = UNSET, x_region: GridInsightsV1LinesGetLatestApparentPowerXRegion | Unset = GridInsightsV1LinesGetLatestApparentPowerXRegion.EU, ) -> Any | GridInsightsV1LinesGetLatestApparentPowerResponse200 | ProblemDetails | None: @@ -147,6 +163,7 @@ def sync( Args: line_id (UUID): + since (datetime.datetime | Unset): Example: 2024-07-01 12:00:00.001000+00:00. x_region (GridInsightsV1LinesGetLatestApparentPowerXRegion | Unset): Default: GridInsightsV1LinesGetLatestApparentPowerXRegion.EU. @@ -161,6 +178,7 @@ def sync( return sync_detailed( line_id=line_id, client=client, + since=since, x_region=x_region, ).parsed @@ -169,6 +187,7 @@ async def asyncio_detailed( line_id: UUID, *, client: AuthenticatedClient | Client, + since: datetime.datetime | Unset = UNSET, x_region: GridInsightsV1LinesGetLatestApparentPowerXRegion | Unset = GridInsightsV1LinesGetLatestApparentPowerXRegion.EU, ) -> Response[Any | GridInsightsV1LinesGetLatestApparentPowerResponse200 | ProblemDetails]: @@ -184,6 +203,7 @@ async def asyncio_detailed( Args: line_id (UUID): + since (datetime.datetime | Unset): Example: 2024-07-01 12:00:00.001000+00:00. x_region (GridInsightsV1LinesGetLatestApparentPowerXRegion | Unset): Default: GridInsightsV1LinesGetLatestApparentPowerXRegion.EU. @@ -197,6 +217,7 @@ async def asyncio_detailed( kwargs = _get_kwargs( line_id=line_id, + since=since, x_region=x_region, ) @@ -209,6 +230,7 @@ async def asyncio( line_id: UUID, *, client: AuthenticatedClient | Client, + since: datetime.datetime | Unset = UNSET, x_region: GridInsightsV1LinesGetLatestApparentPowerXRegion | Unset = GridInsightsV1LinesGetLatestApparentPowerXRegion.EU, ) -> Any | GridInsightsV1LinesGetLatestApparentPowerResponse200 | ProblemDetails | None: @@ -224,6 +246,7 @@ async def asyncio( Args: line_id (UUID): + since (datetime.datetime | Unset): Example: 2024-07-01 12:00:00.001000+00:00. x_region (GridInsightsV1LinesGetLatestApparentPowerXRegion | Unset): Default: GridInsightsV1LinesGetLatestApparentPowerXRegion.EU. @@ -239,6 +262,7 @@ async def asyncio( await asyncio_detailed( line_id=line_id, client=client, + since=since, x_region=x_region, ) ).parsed diff --git a/python/heimdall_api_client/grid_insights_api_client/api/line/grid_insights_v1_lines_get_latest_conductor_temperature.py b/python/heimdall_api_client/grid_insights_api_client/api/line/grid_insights_v1_lines_get_latest_conductor_temperature.py index 966b67d..861f1d8 100644 --- a/python/heimdall_api_client/grid_insights_api_client/api/line/grid_insights_v1_lines_get_latest_conductor_temperature.py +++ b/python/heimdall_api_client/grid_insights_api_client/api/line/grid_insights_v1_lines_get_latest_conductor_temperature.py @@ -1,3 +1,4 @@ +import datetime from http import HTTPStatus from typing import Any, cast from urllib.parse import quote @@ -22,6 +23,7 @@ def _get_kwargs( line_id: UUID, *, unit_system: UnitSystem | Unset = UNSET, + since: datetime.datetime | Unset = UNSET, x_region: GridInsightsV1LinesGetLatestConductorTemperatureXRegion | Unset = GridInsightsV1LinesGetLatestConductorTemperatureXRegion.EU, ) -> dict[str, Any]: @@ -37,6 +39,11 @@ def _get_kwargs( params["unit_system"] = json_unit_system + json_since: str | Unset = UNSET + if not isinstance(since, Unset): + json_since = since.isoformat() + params["since"] = json_since + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} _kwargs: dict[str, Any] = { @@ -104,6 +111,7 @@ def sync_detailed( *, client: AuthenticatedClient | Client, unit_system: UnitSystem | Unset = UNSET, + since: datetime.datetime | Unset = UNSET, x_region: GridInsightsV1LinesGetLatestConductorTemperatureXRegion | Unset = GridInsightsV1LinesGetLatestConductorTemperatureXRegion.EU, ) -> Response[Any | GridInsightsV1LinesGetLatestConductorTemperatureResponse200 | ProblemDetails]: @@ -120,6 +128,7 @@ def sync_detailed( Args: line_id (UUID): unit_system (UnitSystem | Unset): + since (datetime.datetime | Unset): Example: 2024-07-01 12:00:00.001000+00:00. x_region (GridInsightsV1LinesGetLatestConductorTemperatureXRegion | Unset): Default: GridInsightsV1LinesGetLatestConductorTemperatureXRegion.EU. @@ -134,6 +143,7 @@ def sync_detailed( kwargs = _get_kwargs( line_id=line_id, unit_system=unit_system, + since=since, x_region=x_region, ) @@ -149,6 +159,7 @@ def sync( *, client: AuthenticatedClient | Client, unit_system: UnitSystem | Unset = UNSET, + since: datetime.datetime | Unset = UNSET, x_region: GridInsightsV1LinesGetLatestConductorTemperatureXRegion | Unset = GridInsightsV1LinesGetLatestConductorTemperatureXRegion.EU, ) -> Any | GridInsightsV1LinesGetLatestConductorTemperatureResponse200 | ProblemDetails | None: @@ -165,6 +176,7 @@ def sync( Args: line_id (UUID): unit_system (UnitSystem | Unset): + since (datetime.datetime | Unset): Example: 2024-07-01 12:00:00.001000+00:00. x_region (GridInsightsV1LinesGetLatestConductorTemperatureXRegion | Unset): Default: GridInsightsV1LinesGetLatestConductorTemperatureXRegion.EU. @@ -180,6 +192,7 @@ def sync( line_id=line_id, client=client, unit_system=unit_system, + since=since, x_region=x_region, ).parsed @@ -189,6 +202,7 @@ async def asyncio_detailed( *, client: AuthenticatedClient | Client, unit_system: UnitSystem | Unset = UNSET, + since: datetime.datetime | Unset = UNSET, x_region: GridInsightsV1LinesGetLatestConductorTemperatureXRegion | Unset = GridInsightsV1LinesGetLatestConductorTemperatureXRegion.EU, ) -> Response[Any | GridInsightsV1LinesGetLatestConductorTemperatureResponse200 | ProblemDetails]: @@ -205,6 +219,7 @@ async def asyncio_detailed( Args: line_id (UUID): unit_system (UnitSystem | Unset): + since (datetime.datetime | Unset): Example: 2024-07-01 12:00:00.001000+00:00. x_region (GridInsightsV1LinesGetLatestConductorTemperatureXRegion | Unset): Default: GridInsightsV1LinesGetLatestConductorTemperatureXRegion.EU. @@ -219,6 +234,7 @@ async def asyncio_detailed( kwargs = _get_kwargs( line_id=line_id, unit_system=unit_system, + since=since, x_region=x_region, ) @@ -232,6 +248,7 @@ async def asyncio( *, client: AuthenticatedClient | Client, unit_system: UnitSystem | Unset = UNSET, + since: datetime.datetime | Unset = UNSET, x_region: GridInsightsV1LinesGetLatestConductorTemperatureXRegion | Unset = GridInsightsV1LinesGetLatestConductorTemperatureXRegion.EU, ) -> Any | GridInsightsV1LinesGetLatestConductorTemperatureResponse200 | ProblemDetails | None: @@ -248,6 +265,7 @@ async def asyncio( Args: line_id (UUID): unit_system (UnitSystem | Unset): + since (datetime.datetime | Unset): Example: 2024-07-01 12:00:00.001000+00:00. x_region (GridInsightsV1LinesGetLatestConductorTemperatureXRegion | Unset): Default: GridInsightsV1LinesGetLatestConductorTemperatureXRegion.EU. @@ -264,6 +282,7 @@ async def asyncio( line_id=line_id, client=client, unit_system=unit_system, + since=since, x_region=x_region, ) ).parsed diff --git a/python/heimdall_api_client/grid_insights_api_client/api/line/grid_insights_v1_lines_get_latest_current.py b/python/heimdall_api_client/grid_insights_api_client/api/line/grid_insights_v1_lines_get_latest_current.py index 6d2ab57..9ec9c15 100644 --- a/python/heimdall_api_client/grid_insights_api_client/api/line/grid_insights_v1_lines_get_latest_current.py +++ b/python/heimdall_api_client/grid_insights_api_client/api/line/grid_insights_v1_lines_get_latest_current.py @@ -1,3 +1,4 @@ +import datetime from http import HTTPStatus from typing import Any, cast from urllib.parse import quote @@ -12,23 +13,34 @@ ) from ...models.grid_insights_v1_lines_get_latest_current_x_region import GridInsightsV1LinesGetLatestCurrentXRegion from ...models.problem_details import ProblemDetails -from ...types import Response, Unset +from ...types import UNSET, Response, Unset def _get_kwargs( line_id: UUID, *, + since: datetime.datetime | Unset = UNSET, x_region: GridInsightsV1LinesGetLatestCurrentXRegion | Unset = GridInsightsV1LinesGetLatestCurrentXRegion.EU, ) -> dict[str, Any]: headers: dict[str, Any] = {} if not isinstance(x_region, Unset): headers["x-region"] = str(x_region) + params: dict[str, Any] = {} + + json_since: str | Unset = UNSET + if not isinstance(since, Unset): + json_since = since.isoformat() + params["since"] = json_since + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + _kwargs: dict[str, Any] = { "method": "get", "url": "/grid_insights/v1/lines/{line_id}/currents/latest".format( line_id=quote(str(line_id), safe=""), ), + "params": params, } _kwargs["headers"] = headers @@ -87,6 +99,7 @@ def sync_detailed( line_id: UUID, *, client: AuthenticatedClient | Client, + since: datetime.datetime | Unset = UNSET, x_region: GridInsightsV1LinesGetLatestCurrentXRegion | Unset = GridInsightsV1LinesGetLatestCurrentXRegion.EU, ) -> Response[Any | GridInsightsV1LinesGetLatestCurrentResponse200 | ProblemDetails]: """Get latest current @@ -100,6 +113,7 @@ def sync_detailed( Args: line_id (UUID): + since (datetime.datetime | Unset): Example: 2024-07-01 12:00:00.001000+00:00. x_region (GridInsightsV1LinesGetLatestCurrentXRegion | Unset): Default: GridInsightsV1LinesGetLatestCurrentXRegion.EU. @@ -113,6 +127,7 @@ def sync_detailed( kwargs = _get_kwargs( line_id=line_id, + since=since, x_region=x_region, ) @@ -127,6 +142,7 @@ def sync( line_id: UUID, *, client: AuthenticatedClient | Client, + since: datetime.datetime | Unset = UNSET, x_region: GridInsightsV1LinesGetLatestCurrentXRegion | Unset = GridInsightsV1LinesGetLatestCurrentXRegion.EU, ) -> Any | GridInsightsV1LinesGetLatestCurrentResponse200 | ProblemDetails | None: """Get latest current @@ -140,6 +156,7 @@ def sync( Args: line_id (UUID): + since (datetime.datetime | Unset): Example: 2024-07-01 12:00:00.001000+00:00. x_region (GridInsightsV1LinesGetLatestCurrentXRegion | Unset): Default: GridInsightsV1LinesGetLatestCurrentXRegion.EU. @@ -154,6 +171,7 @@ def sync( return sync_detailed( line_id=line_id, client=client, + since=since, x_region=x_region, ).parsed @@ -162,6 +180,7 @@ async def asyncio_detailed( line_id: UUID, *, client: AuthenticatedClient | Client, + since: datetime.datetime | Unset = UNSET, x_region: GridInsightsV1LinesGetLatestCurrentXRegion | Unset = GridInsightsV1LinesGetLatestCurrentXRegion.EU, ) -> Response[Any | GridInsightsV1LinesGetLatestCurrentResponse200 | ProblemDetails]: """Get latest current @@ -175,6 +194,7 @@ async def asyncio_detailed( Args: line_id (UUID): + since (datetime.datetime | Unset): Example: 2024-07-01 12:00:00.001000+00:00. x_region (GridInsightsV1LinesGetLatestCurrentXRegion | Unset): Default: GridInsightsV1LinesGetLatestCurrentXRegion.EU. @@ -188,6 +208,7 @@ async def asyncio_detailed( kwargs = _get_kwargs( line_id=line_id, + since=since, x_region=x_region, ) @@ -200,6 +221,7 @@ async def asyncio( line_id: UUID, *, client: AuthenticatedClient | Client, + since: datetime.datetime | Unset = UNSET, x_region: GridInsightsV1LinesGetLatestCurrentXRegion | Unset = GridInsightsV1LinesGetLatestCurrentXRegion.EU, ) -> Any | GridInsightsV1LinesGetLatestCurrentResponse200 | ProblemDetails | None: """Get latest current @@ -213,6 +235,7 @@ async def asyncio( Args: line_id (UUID): + since (datetime.datetime | Unset): Example: 2024-07-01 12:00:00.001000+00:00. x_region (GridInsightsV1LinesGetLatestCurrentXRegion | Unset): Default: GridInsightsV1LinesGetLatestCurrentXRegion.EU. @@ -228,6 +251,7 @@ async def asyncio( await asyncio_detailed( line_id=line_id, client=client, + since=since, x_region=x_region, ) ).parsed diff --git a/python/heimdall_api_client/grid_insights_api_client/api/line/grid_insights_v1_lines_get_latest_icing.py b/python/heimdall_api_client/grid_insights_api_client/api/line/grid_insights_v1_lines_get_latest_icing.py index 02b72bc..1c3f9f6 100644 --- a/python/heimdall_api_client/grid_insights_api_client/api/line/grid_insights_v1_lines_get_latest_icing.py +++ b/python/heimdall_api_client/grid_insights_api_client/api/line/grid_insights_v1_lines_get_latest_icing.py @@ -124,12 +124,14 @@ def sync_detailed( - **`ice_weight`**: The mass of ice accumulated on the conductor. - **`tension`**: The mechanical tension force in the conductor, which increases as ice accumulates. - **`tension_percentage_of_break_strength`**: Safety-critical metric showing how close the conductor - is to its breaking point. + is to its breaking point or how close it is to the maximum tension it's designed to safely + withstand. - **`timestamp`**: Time (UTC) when the icing measurements were calculated for the span phase. Timestamps may differ per conductor due to data availability. Query parameter `since` sets a cut-off time (UTC) for included icing measurements. Only measurements - with timestamps at or after `since` are considered. If omitted, `since` defaults to 30 minutes ago. + with timestamps at or after `since` are considered. If omitted, the latest icing per span phase is + returned regardless of age. If the latest icing data for a span phase is older than `since`, that span phase is excluded. @@ -187,12 +189,14 @@ def sync( - **`ice_weight`**: The mass of ice accumulated on the conductor. - **`tension`**: The mechanical tension force in the conductor, which increases as ice accumulates. - **`tension_percentage_of_break_strength`**: Safety-critical metric showing how close the conductor - is to its breaking point. + is to its breaking point or how close it is to the maximum tension it's designed to safely + withstand. - **`timestamp`**: Time (UTC) when the icing measurements were calculated for the span phase. Timestamps may differ per conductor due to data availability. Query parameter `since` sets a cut-off time (UTC) for included icing measurements. Only measurements - with timestamps at or after `since` are considered. If omitted, `since` defaults to 30 minutes ago. + with timestamps at or after `since` are considered. If omitted, the latest icing per span phase is + returned regardless of age. If the latest icing data for a span phase is older than `since`, that span phase is excluded. @@ -245,12 +249,14 @@ async def asyncio_detailed( - **`ice_weight`**: The mass of ice accumulated on the conductor. - **`tension`**: The mechanical tension force in the conductor, which increases as ice accumulates. - **`tension_percentage_of_break_strength`**: Safety-critical metric showing how close the conductor - is to its breaking point. + is to its breaking point or how close it is to the maximum tension it's designed to safely + withstand. - **`timestamp`**: Time (UTC) when the icing measurements were calculated for the span phase. Timestamps may differ per conductor due to data availability. Query parameter `since` sets a cut-off time (UTC) for included icing measurements. Only measurements - with timestamps at or after `since` are considered. If omitted, `since` defaults to 30 minutes ago. + with timestamps at or after `since` are considered. If omitted, the latest icing per span phase is + returned regardless of age. If the latest icing data for a span phase is older than `since`, that span phase is excluded. @@ -306,12 +312,14 @@ async def asyncio( - **`ice_weight`**: The mass of ice accumulated on the conductor. - **`tension`**: The mechanical tension force in the conductor, which increases as ice accumulates. - **`tension_percentage_of_break_strength`**: Safety-critical metric showing how close the conductor - is to its breaking point. + is to its breaking point or how close it is to the maximum tension it's designed to safely + withstand. - **`timestamp`**: Time (UTC) when the icing measurements were calculated for the span phase. Timestamps may differ per conductor due to data availability. Query parameter `since` sets a cut-off time (UTC) for included icing measurements. Only measurements - with timestamps at or after `since` are considered. If omitted, `since` defaults to 30 minutes ago. + with timestamps at or after `since` are considered. If omitted, the latest icing per span phase is + returned regardless of age. If the latest icing data for a span phase is older than `since`, that span phase is excluded. diff --git a/python/heimdall_api_client/grid_insights_api_client/api/line/grid_insights_v1_lines_get_latest_sag_and_clearance.py b/python/heimdall_api_client/grid_insights_api_client/api/line/grid_insights_v1_lines_get_latest_sag_and_clearance.py index 4ebf789..effc442 100644 --- a/python/heimdall_api_client/grid_insights_api_client/api/line/grid_insights_v1_lines_get_latest_sag_and_clearance.py +++ b/python/heimdall_api_client/grid_insights_api_client/api/line/grid_insights_v1_lines_get_latest_sag_and_clearance.py @@ -135,7 +135,8 @@ def sync_detailed( phase. Timestamps may differ per conductor due to data availability. Query parameter `since` sets a cut-off time (UTC) for included measurements. Only measurements with - timestamps at or after `since` are considered. If omitted, `since` defaults to 30 minutes ago. + timestamps at or after `since` are considered. If omitted, the latest sag and clearance per span + phase is returned regardless of age. If the latest sag and clearance data for a span phase is older than `since`, that span phase is excluded. @@ -200,7 +201,8 @@ def sync( phase. Timestamps may differ per conductor due to data availability. Query parameter `since` sets a cut-off time (UTC) for included measurements. Only measurements with - timestamps at or after `since` are considered. If omitted, `since` defaults to 30 minutes ago. + timestamps at or after `since` are considered. If omitted, the latest sag and clearance per span + phase is returned regardless of age. If the latest sag and clearance data for a span phase is older than `since`, that span phase is excluded. @@ -260,7 +262,8 @@ async def asyncio_detailed( phase. Timestamps may differ per conductor due to data availability. Query parameter `since` sets a cut-off time (UTC) for included measurements. Only measurements with - timestamps at or after `since` are considered. If omitted, `since` defaults to 30 minutes ago. + timestamps at or after `since` are considered. If omitted, the latest sag and clearance per span + phase is returned regardless of age. If the latest sag and clearance data for a span phase is older than `since`, that span phase is excluded. @@ -323,7 +326,8 @@ async def asyncio( phase. Timestamps may differ per conductor due to data availability. Query parameter `since` sets a cut-off time (UTC) for included measurements. Only measurements with - timestamps at or after `since` are considered. If omitted, `since` defaults to 30 minutes ago. + timestamps at or after `since` are considered. If omitted, the latest sag and clearance per span + phase is returned regardless of age. If the latest sag and clearance data for a span phase is older than `since`, that span phase is excluded. diff --git a/python/scripts/generate-module-client.ps1 b/python/scripts/generate-module-client.ps1 index 7e26fe0..0da11e1 100755 --- a/python/scripts/generate-module-client.ps1 +++ b/python/scripts/generate-module-client.ps1 @@ -46,11 +46,20 @@ try { Write-Host "Could not check PyPI for latest 'openapi-python-client' version." -ForegroundColor DarkYellow } -# Check if ruff is installed for linting -python -m ruff --version 2>$null +# Ensure ruff is available to the interpreter that runs the generator. +# openapi-python-client formats its output via the post_hooks in +# openapi_python_client_config.yaml, which call 'python -m ruff'. Probing the +# module (not the 'ruff' executable) is what those hooks actually need: pip +# installs ruff.exe into a Scripts directory that is often not on PATH, and the +# generator only warns when a hook fails, leaving the output unformatted. +python -m ruff --version 2>$null | Out-Null if ($LASTEXITCODE -ne 0) { Write-Host "Installing 'ruff'..." - python -m pip install ruff + python -m pip install ruff --quiet + python -m ruff --version 2>$null | Out-Null + if ($LASTEXITCODE -ne 0) { + throw "'python -m ruff' is still unavailable after install; generated code would be left unformatted." + } } else { Write-Host "'ruff' is already installed." @@ -61,7 +70,7 @@ if (!(Test-Path $specDir)) { New-Item -ItemType Directory -Force -Path $specDir | Out-Null } else { - Remove-Item -Recurse -Force "$specDir/*" -ErrorAction Silently + Remove-Item -Recurse -Force "$specDir/*" -ErrorAction SilentlyContinue } # Download the OpenAPI spec @@ -73,6 +82,17 @@ Write-Host "Generating client for module '$Module'..." python -m openapi_python_client generate ` --path $specPath --overwrite --output-path $generatedFolder --config openapi_python_client_config.yaml +# Verify the post_hooks actually formatted the output. A hook failure is only a +# warning to the generator, and unformatted code shows up as a diff touching +# every file in the package rather than just the endpoints that changed. +# Runs before the metadata cleanup below so ruff resolves the same config the +# post_hooks used: the generated project's own pyproject.toml. +Write-Host "Verifying generated code is formatted..." +python -m ruff format --check $generatedFolder | Out-Null +if ($LASTEXITCODE -ne 0) { + throw "Generated code is not formatted: the post_hooks in openapi_python_client_config.yaml did not run. Aborting rather than producing a whole-package diff." +} + # Remove the generated README, .gitignore, and pyproject.toml files Write-Host "Cleaning up generated files in..." Remove-Item -Path "$generatedFolder/README.md" -ErrorAction SilentlyContinue diff --git a/python/scripts/openapi_python_client_config.yaml b/python/scripts/openapi_python_client_config.yaml index 69c3ceb..8bbbeaf 100644 --- a/python/scripts/openapi_python_client_config.yaml +++ b/python/scripts/openapi_python_client_config.yaml @@ -1,2 +1,11 @@ # This file is used to configure the OpenAPI Python client generation process. meta: none # Igonre the project metadata + +# openapi-python-client formats its own output with these hooks. Its defaults +# invoke the bare `ruff` executable, which silently no-ops when ruff is +# installed but its Scripts directory is not on PATH -- producing unformatted +# generated code and a huge spurious diff. Invoking it through the interpreter +# that runs the generator makes the hooks independent of PATH. +post_hooks: + - "python -m ruff check . --fix-only --extend-select=I" + - "python -m ruff format ." diff --git a/python/tests/integration/conftest.py b/python/tests/integration/conftest.py index a14b773..8e46d2c 100644 --- a/python/tests/integration/conftest.py +++ b/python/tests/integration/conftest.py @@ -1,8 +1,13 @@ +import datetime import os import pytest from heimdall_api_client import HeimdallApiClient +from heimdall_api_client.errors import HeimdallApiError + +# The API rejects windows longer than 30 days. +_WINDOW = datetime.timedelta(days=1) @pytest.fixture(scope="session") @@ -10,3 +15,51 @@ def api_client(): return HeimdallApiClient( client_id=os.environ["HEIMDALL_CLIENT_ID"], client_secret=os.environ["HEIMDALL_CLIENT_SECRET"] ) + + +@pytest.fixture(scope="session") +def line_id(api_client): + """The first line found on any facility of the first grid owner.""" + assets = api_client.get_assets() + for grid_owner in assets.data.grid_owners: + for facility in grid_owner.facilities: + if facility.line: + return facility.line.id + pytest.skip("No facility with a line available for this client") + + +@pytest.fixture(scope="session") +def facility_id(api_client): + assets = api_client.get_assets() + for grid_owner in assets.data.grid_owners: + if grid_owner.facilities: + return grid_owner.facilities[0].id + pytest.skip("No facility available for this client") + + +@pytest.fixture(scope="session") +def window(): + to_timestamp = datetime.datetime.now(datetime.UTC) + return to_timestamp - _WINDOW, to_timestamp + + +@pytest.fixture +def assert_endpoint_responds(): + """ + Asserts the endpoint is wired up correctly: it must return a well-formed + response, or report 404 because the asset has no data for this metric. + Any other status means the wiring or the request itself is wrong. + """ + + def assert_responds(call, description: str): + try: + response = call() + except HeimdallApiError as e: + if e.status_code == 404: + pytest.skip(f"No data available for {description}") + raise + + assert response is not None, f"{description} should not return None" + assert hasattr(response, "data"), f"{description} response should have a 'data' attribute" + + return assert_responds diff --git a/python/tests/integration/test_when_fetching_historical_data.py b/python/tests/integration/test_when_fetching_historical_data.py new file mode 100644 index 0000000..58cb16e --- /dev/null +++ b/python/tests/integration/test_when_fetching_historical_data.py @@ -0,0 +1,119 @@ +""" +Endpoints that return a series of measurements over a from/to window. +Latest-value endpoints are covered in test_when_fetching_latest_data.py. +""" + +import datetime + +import pytest + +from heimdall_api_client.capacity_monitoring_api_client.models.quantity import Quantity +from heimdall_api_client.errors import HeimdallApiError +from heimdall_api_client.grid_insights_api_client.models.unit_system import UnitSystem + + +@pytest.mark.integration +@pytest.mark.parametrize( + "method_name", + [ + "get_currents", + "get_conductor_temperatures", + "get_icing", + "get_sag_and_clearance", + "get_apparent_power", + "get_heimdall_dlrs", + "get_heimdall_aars", + ], +) +def test_should_return_historical_line_data(api_client, line_id, window, assert_endpoint_responds, method_name): + from_timestamp, to_timestamp = window + + assert_endpoint_responds( + lambda: getattr(api_client, method_name)(line_id, from_timestamp, to_timestamp), + f"{method_name} on line {line_id}", + ) + + +@pytest.mark.integration +def test_should_return_historical_circuit_ratings(api_client, facility_id, window, assert_endpoint_responds): + from_timestamp, to_timestamp = window + + assert_endpoint_responds( + lambda: api_client.get_circuit_ratings(facility_id, from_timestamp, to_timestamp), + f"circuit ratings on facility {facility_id}", + ) + + +@pytest.mark.integration +def test_should_accept_naive_and_offset_timestamps(api_client, line_id, assert_endpoint_responds): + """ + The API accepts only Z-suffixed UTC, so the SDK must normalize whatever the + caller passes -- naive, UTC-aware, or another offset -- to the same window. + """ + oslo = datetime.timezone(datetime.timedelta(hours=2)) + naive_from = datetime.datetime(2026, 7, 1, 10, 0, 0) + naive_to = datetime.datetime(2026, 7, 2, 10, 0, 0) + + equivalent_windows = [ + (naive_from, naive_to), + (naive_from.replace(tzinfo=datetime.UTC), naive_to.replace(tzinfo=datetime.UTC)), + (naive_from.replace(tzinfo=oslo), naive_to.replace(tzinfo=oslo)), + (naive_from.replace(microsecond=123456, tzinfo=datetime.UTC), naive_to.replace(tzinfo=datetime.UTC)), + ] + + for from_timestamp, to_timestamp in equivalent_windows: + assert_endpoint_responds( + lambda f=from_timestamp, t=to_timestamp: api_client.get_currents(line_id, f, t), + f"currents with from={from_timestamp.isoformat()} to={to_timestamp.isoformat()}", + ) + + +@pytest.mark.integration +@pytest.mark.parametrize("method_name", ["get_conductor_temperatures", "get_icing", "get_sag_and_clearance"]) +@pytest.mark.parametrize("unit_system", [UnitSystem.METRIC, "imperial"]) +def test_should_accept_unit_system(api_client, line_id, window, assert_endpoint_responds, method_name, unit_system): + from_timestamp, to_timestamp = window + + assert_endpoint_responds( + lambda: getattr(api_client, method_name)(line_id, from_timestamp, to_timestamp, unit_system=unit_system), + f"{method_name} with unit_system={unit_system}", + ) + + +@pytest.mark.integration +@pytest.mark.parametrize("method_name", ["get_heimdall_dlrs", "get_heimdall_aars"]) +@pytest.mark.parametrize("quantity", [Quantity.CURRENT, "apparent_power"]) +def test_should_accept_quantity_for_line_ratings( + api_client, line_id, window, assert_endpoint_responds, method_name, quantity +): + from_timestamp, to_timestamp = window + + assert_endpoint_responds( + lambda: getattr(api_client, method_name)(line_id, from_timestamp, to_timestamp, quantity=quantity), + f"{method_name} with quantity={quantity}", + ) + + +@pytest.mark.integration +@pytest.mark.parametrize("quantity", [Quantity.CURRENT, "apparent_power"]) +def test_should_accept_quantity_for_circuit_ratings( + api_client, facility_id, window, assert_endpoint_responds, quantity +): + from_timestamp, to_timestamp = window + + assert_endpoint_responds( + lambda: api_client.get_circuit_ratings(facility_id, from_timestamp, to_timestamp, quantity=quantity), + f"circuit ratings with quantity={quantity}", + ) + + +@pytest.mark.integration +def test_should_reject_window_longer_than_30_days(api_client, line_id): + """The API caps the range at 30 days; the SDK surfaces that as HeimdallApiError.""" + to_timestamp = datetime.datetime.now(datetime.UTC) + from_timestamp = to_timestamp - datetime.timedelta(days=45) + + with pytest.raises(HeimdallApiError) as excinfo: + api_client.get_currents(line_id, from_timestamp, to_timestamp) + + assert excinfo.value.status_code == 400 diff --git a/python/tests/integration/test_when_fetching_latest_data.py b/python/tests/integration/test_when_fetching_latest_data.py new file mode 100644 index 0000000..d9fe492 --- /dev/null +++ b/python/tests/integration/test_when_fetching_latest_data.py @@ -0,0 +1,102 @@ +""" +Endpoints that return the latest value for an asset, plus forecasts. +Windowed endpoints are covered in test_when_fetching_historical_data.py. +""" + +import datetime + +import pytest + +from heimdall_api_client.grid_insights_api_client.models.unit_system import UnitSystem + +_LATEST_LINE_METHODS_ACCEPTING_SINCE = [ + "get_latest_current", + "get_latest_conductor_temperature", + "get_latest_apparent_power", + "get_latest_icing", + "get_latest_sag_and_clearance", + "get_latest_heimdall_dlr", + "get_latest_heimdall_aar", +] + + +@pytest.mark.integration +@pytest.mark.parametrize( + "method_name", + [ + "get_latest_current", + "get_latest_conductor_temperature", + "get_latest_apparent_power", + "get_latest_icing", + "get_latest_sag_and_clearance", + "get_latest_heimdall_dlr", + "get_latest_heimdall_aar", + "get_latest_heimdall_dlr_forecasts", + "get_latest_heimdall_aar_forecasts", + ], +) +def test_should_return_latest_line_data(api_client, line_id, assert_endpoint_responds, method_name): + assert_endpoint_responds( + lambda: getattr(api_client, method_name)(line_id), + f"{method_name} on line {line_id}", + ) + + +@pytest.mark.integration +@pytest.mark.parametrize("method_name", ["get_latest_circuit_rating", "get_latest_circuit_rating_forecasts"]) +def test_should_return_latest_facility_data(api_client, facility_id, assert_endpoint_responds, method_name): + assert_endpoint_responds( + lambda: getattr(api_client, method_name)(facility_id), + f"{method_name} on facility {facility_id}", + ) + + +@pytest.mark.integration +@pytest.mark.parametrize("unit_system", [UnitSystem.METRIC, "imperial"]) +def test_should_return_icing_forecast(api_client, line_id, assert_endpoint_responds, unit_system): + assert_endpoint_responds( + lambda: api_client.get_icing_forecast(line_id, unit_system=unit_system), + f"icing forecast with unit_system={unit_system}", + ) + + +@pytest.mark.integration +@pytest.mark.parametrize("method_name", _LATEST_LINE_METHODS_ACCEPTING_SINCE) +def test_should_accept_since_on_latest_line_endpoints(api_client, line_id, assert_endpoint_responds, method_name): + """ + `since` bounds how old the returned value may be. It is serialized with the + same isoformat() the API rejects unless the SDK normalizes it, so a 404 here + means no value is newer than `since` -- not that the parameter was dropped. + """ + since = datetime.datetime.now(datetime.UTC) - datetime.timedelta(hours=6) + + assert_endpoint_responds( + lambda: getattr(api_client, method_name)(line_id, since=since), + f"{method_name} with since={since.isoformat()}", + ) + + +@pytest.mark.integration +def test_should_accept_since_on_latest_circuit_rating(api_client, facility_id, assert_endpoint_responds): + since = datetime.datetime.now(datetime.UTC) - datetime.timedelta(hours=6) + + assert_endpoint_responds( + lambda: api_client.get_latest_circuit_rating(facility_id, since=since), + f"latest circuit rating with since={since.isoformat()}", + ) + + +@pytest.mark.integration +@pytest.mark.parametrize("method_name", _LATEST_LINE_METHODS_ACCEPTING_SINCE) +def test_should_return_a_value_older_than_a_generous_since(api_client, line_id, assert_endpoint_responds, method_name): + """ + A `since` far enough back that any stored value qualifies. This distinguishes + a working `since` from one the API rejects outright: a rejected timestamp + fails with 400 regardless of how much data exists. + """ + since = datetime.datetime.now(datetime.UTC) - datetime.timedelta(days=730) + + assert_endpoint_responds( + lambda: getattr(api_client, method_name)(line_id, since=since), + f"{method_name} with since={since.isoformat()}", + ) diff --git a/python/tests/unit/test_endpoint_wrappers_resolve.py b/python/tests/unit/test_endpoint_wrappers_resolve.py new file mode 100644 index 0000000..cec1d3d --- /dev/null +++ b/python/tests/unit/test_endpoint_wrappers_resolve.py @@ -0,0 +1,92 @@ +""" +HeimdallApiClient imports most endpoint wrappers inside the method body, which +means a wrapper that was never written stays invisible to ruff and to import of +the package — it only fails when a caller reaches the method. These tests +resolve every wrapper the client methods import, so a missing one fails in CI. +""" + +import pytest + +from heimdall_api_client import capacity_monitoring, grid_insights + +_GRID_INSIGHTS_WRAPPERS = [ + "get_latest_conductor_temperature", + "get_latest_current", + "get_latest_icing", + "get_latest_sag_and_clearance", + "get_currents", + "get_conductor_temperatures", + "get_icing", + "get_sag_and_clearance", + "get_apparent_power", + "get_latest_apparent_power", + "get_icing_forecast", +] + +_CAPACITY_MONITORING_WRAPPERS = [ + "get_latest_heimdall_dlr", + "get_latest_heimdall_aar", + "get_latest_heimdall_dlr_forecasts", + "get_latest_heimdall_arr_forecasts", + "get_latest_circuit_ratring", + "get_latest_circuit_rating_forecasts", + "get_heimdall_dlrs", + "get_heimdall_aars", + "get_circuit_ratings", +] + +_CLIENT_METHODS = [ + "get_assets", + "get_currents", + "get_conductor_temperatures", + "get_icing", + "get_sag_and_clearance", + "get_apparent_power", + "get_latest_apparent_power", + "get_icing_forecast", + "get_heimdall_dlrs", + "get_heimdall_aars", + "get_circuit_ratings", +] + + +@pytest.mark.parametrize("name", _GRID_INSIGHTS_WRAPPERS) +def test_grid_insights_wrapper_is_defined(name: str): + assert callable(getattr(grid_insights, name, None)), f"grid_insights.{name} is missing" + + +@pytest.mark.parametrize("name", _CAPACITY_MONITORING_WRAPPERS) +def test_capacity_monitoring_wrapper_is_defined(name: str): + assert callable(getattr(capacity_monitoring, name, None)), f"capacity_monitoring.{name} is missing" + + +@pytest.mark.parametrize("name", _CLIENT_METHODS) +def test_client_exposes_method(name: str): + from heimdall_api_client import HeimdallApiClient + + assert callable(getattr(HeimdallApiClient, name, None)), f"HeimdallApiClient.{name} is missing" + + +# The API added `since` to these after the wrappers were first written; the +# generated endpoints accept it, so the wrappers must not silently drop it. +_METHODS_ACCEPTING_SINCE = [ + "get_latest_current", + "get_latest_conductor_temperature", + "get_latest_apparent_power", + "get_latest_icing", + "get_latest_sag_and_clearance", + "get_latest_heimdall_dlr", + "get_latest_heimdall_aar", + "get_latest_circuit_rating", +] + + +@pytest.mark.parametrize("name", _METHODS_ACCEPTING_SINCE) +def test_client_method_accepts_since(name: str): + import inspect + + from heimdall_api_client import HeimdallApiClient + + parameters = inspect.signature(getattr(HeimdallApiClient, name)).parameters + assert "since" in parameters, f"HeimdallApiClient.{name} should accept since" + assert parameters["since"].default is None, f"HeimdallApiClient.{name} should default since to None" diff --git a/python/tests/unit/test_timestamp_normalization.py b/python/tests/unit/test_timestamp_normalization.py new file mode 100644 index 0000000..177f507 --- /dev/null +++ b/python/tests/unit/test_timestamp_normalization.py @@ -0,0 +1,49 @@ +""" +Unit tests for as_zulu, which exists because the API accepts only Z-suffixed UTC +timestamps while datetime.isoformat() always emits a `+00:00` offset. +""" + +import datetime + +import pytest + +from heimdall_api_client._timestamps import as_zulu + +_OSLO = datetime.timezone(datetime.timedelta(hours=2)) + + +class TestAsZulu: + def test_naive_is_treated_as_utc(self): + assert as_zulu(datetime.datetime(2024, 7, 2, 10, 0, 0)).isoformat() == "2024-07-02T10:00:00Z" + + def test_utc_aware_keeps_its_wall_clock(self): + timestamp = datetime.datetime(2024, 7, 2, 10, 0, 0, tzinfo=datetime.UTC) + assert as_zulu(timestamp).isoformat() == "2024-07-02T10:00:00Z" + + def test_other_offset_is_converted_to_utc(self): + timestamp = datetime.datetime(2024, 7, 2, 12, 0, 0, tzinfo=_OSLO) + assert as_zulu(timestamp).isoformat() == "2024-07-02T10:00:00Z" + + def test_microseconds_are_preserved(self): + timestamp = datetime.datetime(2024, 7, 2, 10, 0, 0, 123456, tzinfo=datetime.UTC) + assert as_zulu(timestamp).isoformat() == "2024-07-02T10:00:00.123456Z" + + @pytest.mark.parametrize( + "timestamp", + [ + datetime.datetime(2024, 7, 2, 10, 0, 0), + datetime.datetime(2024, 7, 2, 10, 0, 0, tzinfo=datetime.UTC), + datetime.datetime(2024, 7, 2, 12, 0, 0, tzinfo=_OSLO), + ], + ) + def test_never_emits_an_offset(self, timestamp: datetime.datetime): + rendered = as_zulu(timestamp).isoformat() + assert rendered.endswith("Z") + assert "+" not in rendered + + def test_represents_the_same_instant(self): + timestamp = datetime.datetime(2024, 7, 2, 12, 0, 0, tzinfo=_OSLO) + assert as_zulu(timestamp).timestamp() == timestamp.timestamp() + + def test_is_a_datetime_so_generated_clients_accept_it(self): + assert isinstance(as_zulu(datetime.datetime(2024, 7, 2)), datetime.datetime)