Skip to content
50 changes: 50 additions & 0 deletions python/heimdall_api_client/_timestamps.py
Original file line number Diff line number Diff line change
@@ -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,
)
149 changes: 143 additions & 6 deletions python/heimdall_api_client/capacity_monitoring.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
)
Expand All @@ -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(
Expand All @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import datetime
from http import HTTPStatus
from typing import Any, cast
from urllib.parse import quote
Expand All @@ -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]:
Expand All @@ -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] = {
Expand Down Expand Up @@ -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]:
Expand Down Expand Up @@ -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.

Expand All @@ -161,6 +170,7 @@ def sync_detailed(
kwargs = _get_kwargs(
facility_id=facility_id,
quantity=quantity,
since=since,
x_region=x_region,
)

Expand All @@ -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:
Expand Down Expand Up @@ -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.

Expand All @@ -234,6 +246,7 @@ def sync(
facility_id=facility_id,
client=client,
quantity=quantity,
since=since,
x_region=x_region,
).parsed

Expand All @@ -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]:
Expand Down Expand Up @@ -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.

Expand All @@ -300,6 +315,7 @@ async def asyncio_detailed(
kwargs = _get_kwargs(
facility_id=facility_id,
quantity=quantity,
since=since,
x_region=x_region,
)

Expand All @@ -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:
Expand Down Expand Up @@ -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.

Expand All @@ -372,6 +390,7 @@ async def asyncio(
facility_id=facility_id,
client=client,
quantity=quantity,
since=since,
x_region=x_region,
)
).parsed
Loading
Loading