From 96fa158e382970fb6fa8c7d3c5aafda730b3c38a Mon Sep 17 00:00:00 2001 From: "Joseph T. French" Date: Thu, 23 Jul 2026 02:50:54 -0500 Subject: [PATCH] feat: compute-forecast + scenario-aware models (F-1 regen) OpenAPI regen against the F-1 backend: the compute-forecast operation (ComputeForecastRequest/Response, ForecastMonthLite, SkippedForecastLite), the forecast block-type authoring arms (ForecastMechanics, lever assertions, Create/Update/DeleteForecastRequest), scenario_id on ComputeMetricsRequest and FactSetLite. --- .../compute_forecast.py | 314 ++++++++++++++++++ .../create_information_block.py | 29 +- .../delete_information_block.py | 29 +- .../update_information_block.py | 29 +- robosystems_client/models/__init__.py | 48 +++ .../models/artifact_response.py | 31 +- .../models/compute_forecast_request.py | 115 +++++++ .../models/compute_forecast_response.py | 146 ++++++++ .../models/compute_metrics_request.py | 23 ++ .../models/create_forecast_arm.py | 88 +++++ .../models/create_forecast_request.py | 166 +++++++++ .../create_forecast_request_scenario_kind.py | 10 + .../models/delete_forecast_arm.py | 85 +++++ .../models/delete_forecast_request.py | 66 ++++ robosystems_client/models/fact_set_lite.py | 23 +- .../models/forecast_mechanics.py | 146 ++++++++ .../forecast_mechanics_scenario_kind.py | 10 + .../models/forecast_month_lite.py | 135 ++++++++ .../models/lever_assertion_lite.py | 120 +++++++ .../lever_assertion_lite_values_by_period.py | 47 +++ .../models/lever_assertion_request.py | 144 ++++++++ ...sertion_request_values_by_period_type_0.py | 47 +++ ...tion_envelope_compute_forecast_response.py | 158 +++++++++ ...velope_compute_forecast_response_status.py | 10 + .../models/skipped_forecast_lite.py | 126 +++++++ .../models/update_forecast_arm.py | 88 +++++ .../models/update_forecast_request.py | 210 ++++++++++++ ...e_forecast_request_scenario_kind_type_0.py | 10 + 28 files changed, 2408 insertions(+), 45 deletions(-) create mode 100644 robosystems_client/api/extensions_robo_ledger/compute_forecast.py create mode 100644 robosystems_client/models/compute_forecast_request.py create mode 100644 robosystems_client/models/compute_forecast_response.py create mode 100644 robosystems_client/models/create_forecast_arm.py create mode 100644 robosystems_client/models/create_forecast_request.py create mode 100644 robosystems_client/models/create_forecast_request_scenario_kind.py create mode 100644 robosystems_client/models/delete_forecast_arm.py create mode 100644 robosystems_client/models/delete_forecast_request.py create mode 100644 robosystems_client/models/forecast_mechanics.py create mode 100644 robosystems_client/models/forecast_mechanics_scenario_kind.py create mode 100644 robosystems_client/models/forecast_month_lite.py create mode 100644 robosystems_client/models/lever_assertion_lite.py create mode 100644 robosystems_client/models/lever_assertion_lite_values_by_period.py create mode 100644 robosystems_client/models/lever_assertion_request.py create mode 100644 robosystems_client/models/lever_assertion_request_values_by_period_type_0.py create mode 100644 robosystems_client/models/operation_envelope_compute_forecast_response.py create mode 100644 robosystems_client/models/operation_envelope_compute_forecast_response_status.py create mode 100644 robosystems_client/models/skipped_forecast_lite.py create mode 100644 robosystems_client/models/update_forecast_arm.py create mode 100644 robosystems_client/models/update_forecast_request.py create mode 100644 robosystems_client/models/update_forecast_request_scenario_kind_type_0.py diff --git a/robosystems_client/api/extensions_robo_ledger/compute_forecast.py b/robosystems_client/api/extensions_robo_ledger/compute_forecast.py new file mode 100644 index 0000000..7f1b71c --- /dev/null +++ b/robosystems_client/api/extensions_robo_ledger/compute_forecast.py @@ -0,0 +1,314 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.compute_forecast_request import ComputeForecastRequest +from ...models.error_response import ErrorResponse +from ...models.operation_envelope_compute_forecast_response import ( + OperationEnvelopeComputeForecastResponse, +) +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + graph_id: str, + *, + body: ComputeForecastRequest, + idempotency_key: None | str | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + if not isinstance(idempotency_key, Unset): + headers["Idempotency-Key"] = idempotency_key + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/extensions/roboledger/{graph_id}/operations/compute-forecast".format( + graph_id=quote(str(graph_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ErrorResponse | OperationEnvelopeComputeForecastResponse | None: + if response.status_code == 200: + response_200 = OperationEnvelopeComputeForecastResponse.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ErrorResponse.from_dict(response.json()) + + return response_401 + + if response.status_code == 403: + response_403 = ErrorResponse.from_dict(response.json()) + + return response_403 + + if response.status_code == 404: + response_404 = ErrorResponse.from_dict(response.json()) + + return response_404 + + if response.status_code == 409: + response_409 = ErrorResponse.from_dict(response.json()) + + return response_409 + + if response.status_code == 422: + response_422 = ErrorResponse.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = ErrorResponse.from_dict(response.json()) + + return response_429 + + if response.status_code == 500: + response_500 = ErrorResponse.from_dict(response.json()) + + return response_500 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ErrorResponse | OperationEnvelopeComputeForecastResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + graph_id: str, + *, + client: AuthenticatedClient, + body: ComputeForecastRequest, + idempotency_key: None | str | Unset = UNSET, +) -> Response[ErrorResponse | OperationEnvelopeComputeForecastResponse]: + """Compute Forecast for a Forecast Block + + Walks a forecast block's driver cascade month-by-month forward from its base period: lever-driven + rs-driver Derive rules in dependency order, carry-forward for unmodeled income-statement lines, + calc-DAG subtotals — upserting one scenario income-statement FactSet (plus a working-capital + balance-sheet set) per forward month, all keyed by the block's scenario_id (NULL = actuals; scenario + reads pass it as a filter). Re-running replaces each month's values. Rules with missing lever months + or unbound operands are skipped with a reason (their targets fall back to carry-forward), never + errored. Deterministic and non-AI — no credits consumed. + + **Idempotency**: supply an `Idempotency-Key` header to make safe retries; replays within 24 hours + return the same envelope. Reusing the key with a different body returns HTTP 409 Conflict. + + Args: + graph_id (str): + idempotency_key (None | str | Unset): + body (ComputeForecastRequest): Request body for the ``compute-forecast`` operation. + + Walks the scenario's driver cascade month-by-month forward from the + forecast block's ``base_period``: lever-driven Derive rules in + dependency order, carry-forward for unmodeled IS lines, calc-DAG + subtotals — upserting one scenario IS FactSet (+ a working-capital BS + set) per forward month, all keyed by the forecast block's + ``scenario_id``. Re-running replaces each month's values (the + compute-metrics drift semantics). Deterministic and non-AI — no + credits consumed. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | OperationEnvelopeComputeForecastResponse] + """ + + kwargs = _get_kwargs( + graph_id=graph_id, + body=body, + idempotency_key=idempotency_key, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + graph_id: str, + *, + client: AuthenticatedClient, + body: ComputeForecastRequest, + idempotency_key: None | str | Unset = UNSET, +) -> ErrorResponse | OperationEnvelopeComputeForecastResponse | None: + """Compute Forecast for a Forecast Block + + Walks a forecast block's driver cascade month-by-month forward from its base period: lever-driven + rs-driver Derive rules in dependency order, carry-forward for unmodeled income-statement lines, + calc-DAG subtotals — upserting one scenario income-statement FactSet (plus a working-capital + balance-sheet set) per forward month, all keyed by the block's scenario_id (NULL = actuals; scenario + reads pass it as a filter). Re-running replaces each month's values. Rules with missing lever months + or unbound operands are skipped with a reason (their targets fall back to carry-forward), never + errored. Deterministic and non-AI — no credits consumed. + + **Idempotency**: supply an `Idempotency-Key` header to make safe retries; replays within 24 hours + return the same envelope. Reusing the key with a different body returns HTTP 409 Conflict. + + Args: + graph_id (str): + idempotency_key (None | str | Unset): + body (ComputeForecastRequest): Request body for the ``compute-forecast`` operation. + + Walks the scenario's driver cascade month-by-month forward from the + forecast block's ``base_period``: lever-driven Derive rules in + dependency order, carry-forward for unmodeled IS lines, calc-DAG + subtotals — upserting one scenario IS FactSet (+ a working-capital BS + set) per forward month, all keyed by the forecast block's + ``scenario_id``. Re-running replaces each month's values (the + compute-metrics drift semantics). Deterministic and non-AI — no + credits consumed. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | OperationEnvelopeComputeForecastResponse + """ + + return sync_detailed( + graph_id=graph_id, + client=client, + body=body, + idempotency_key=idempotency_key, + ).parsed + + +async def asyncio_detailed( + graph_id: str, + *, + client: AuthenticatedClient, + body: ComputeForecastRequest, + idempotency_key: None | str | Unset = UNSET, +) -> Response[ErrorResponse | OperationEnvelopeComputeForecastResponse]: + """Compute Forecast for a Forecast Block + + Walks a forecast block's driver cascade month-by-month forward from its base period: lever-driven + rs-driver Derive rules in dependency order, carry-forward for unmodeled income-statement lines, + calc-DAG subtotals — upserting one scenario income-statement FactSet (plus a working-capital + balance-sheet set) per forward month, all keyed by the block's scenario_id (NULL = actuals; scenario + reads pass it as a filter). Re-running replaces each month's values. Rules with missing lever months + or unbound operands are skipped with a reason (their targets fall back to carry-forward), never + errored. Deterministic and non-AI — no credits consumed. + + **Idempotency**: supply an `Idempotency-Key` header to make safe retries; replays within 24 hours + return the same envelope. Reusing the key with a different body returns HTTP 409 Conflict. + + Args: + graph_id (str): + idempotency_key (None | str | Unset): + body (ComputeForecastRequest): Request body for the ``compute-forecast`` operation. + + Walks the scenario's driver cascade month-by-month forward from the + forecast block's ``base_period``: lever-driven Derive rules in + dependency order, carry-forward for unmodeled IS lines, calc-DAG + subtotals — upserting one scenario IS FactSet (+ a working-capital BS + set) per forward month, all keyed by the forecast block's + ``scenario_id``. Re-running replaces each month's values (the + compute-metrics drift semantics). Deterministic and non-AI — no + credits consumed. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | OperationEnvelopeComputeForecastResponse] + """ + + kwargs = _get_kwargs( + graph_id=graph_id, + body=body, + idempotency_key=idempotency_key, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + graph_id: str, + *, + client: AuthenticatedClient, + body: ComputeForecastRequest, + idempotency_key: None | str | Unset = UNSET, +) -> ErrorResponse | OperationEnvelopeComputeForecastResponse | None: + """Compute Forecast for a Forecast Block + + Walks a forecast block's driver cascade month-by-month forward from its base period: lever-driven + rs-driver Derive rules in dependency order, carry-forward for unmodeled income-statement lines, + calc-DAG subtotals — upserting one scenario income-statement FactSet (plus a working-capital + balance-sheet set) per forward month, all keyed by the block's scenario_id (NULL = actuals; scenario + reads pass it as a filter). Re-running replaces each month's values. Rules with missing lever months + or unbound operands are skipped with a reason (their targets fall back to carry-forward), never + errored. Deterministic and non-AI — no credits consumed. + + **Idempotency**: supply an `Idempotency-Key` header to make safe retries; replays within 24 hours + return the same envelope. Reusing the key with a different body returns HTTP 409 Conflict. + + Args: + graph_id (str): + idempotency_key (None | str | Unset): + body (ComputeForecastRequest): Request body for the ``compute-forecast`` operation. + + Walks the scenario's driver cascade month-by-month forward from the + forecast block's ``base_period``: lever-driven Derive rules in + dependency order, carry-forward for unmodeled IS lines, calc-DAG + subtotals — upserting one scenario IS FactSet (+ a working-capital BS + set) per forward month, all keyed by the forecast block's + ``scenario_id``. Re-running replaces each month's values (the + compute-metrics drift semantics). Deterministic and non-AI — no + credits consumed. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | OperationEnvelopeComputeForecastResponse + """ + + return ( + await asyncio_detailed( + graph_id=graph_id, + client=client, + body=body, + idempotency_key=idempotency_key, + ) + ).parsed diff --git a/robosystems_client/api/extensions_robo_ledger/create_information_block.py b/robosystems_client/api/extensions_robo_ledger/create_information_block.py index af8b438..5cd49a9 100644 --- a/robosystems_client/api/extensions_robo_ledger/create_information_block.py +++ b/robosystems_client/api/extensions_robo_ledger/create_information_block.py @@ -6,6 +6,7 @@ from ... import errors from ...client import AuthenticatedClient, Client +from ...models.create_forecast_arm import CreateForecastArm from ...models.create_legacy_arm import CreateLegacyArm from ...models.create_rollforward_arm import CreateRollforwardArm from ...models.create_schedule_arm import CreateScheduleArm @@ -19,7 +20,7 @@ def _get_kwargs( graph_id: str, *, - body: CreateLegacyArm | CreateRollforwardArm | CreateScheduleArm, + body: CreateForecastArm | CreateLegacyArm | CreateRollforwardArm | CreateScheduleArm, idempotency_key: None | str | Unset = UNSET, ) -> dict[str, Any]: headers: dict[str, Any] = {} @@ -37,6 +38,8 @@ def _get_kwargs( _kwargs["json"] = body.to_dict() elif isinstance(body, CreateRollforwardArm): _kwargs["json"] = body.to_dict() + elif isinstance(body, CreateForecastArm): + _kwargs["json"] = body.to_dict() else: _kwargs["json"] = body.to_dict() @@ -115,7 +118,7 @@ def sync_detailed( graph_id: str, *, client: AuthenticatedClient, - body: CreateLegacyArm | CreateRollforwardArm | CreateScheduleArm, + body: CreateForecastArm | CreateLegacyArm | CreateRollforwardArm | CreateScheduleArm, idempotency_key: None | str | Unset = UNSET, ) -> Response[ErrorResponse | OperationEnvelopeInformationBlockEnvelope]: """Create Information Block @@ -133,8 +136,8 @@ def sync_detailed( Args: graph_id (str): idempotency_key (None | str | Unset): - body (CreateLegacyArm | CreateRollforwardArm | CreateScheduleArm): Create an Information - Block. The body is a discriminated union on + body (CreateForecastArm | CreateLegacyArm | CreateRollforwardArm | CreateScheduleArm): + Create an Information Block. The body is a discriminated union on `block_type`: pick the arm matching the block type you want to create. The schedule arm carries a fully typed payload; statement and metric arms accept an untyped payload but currently return HTTP @@ -166,7 +169,7 @@ def sync( graph_id: str, *, client: AuthenticatedClient, - body: CreateLegacyArm | CreateRollforwardArm | CreateScheduleArm, + body: CreateForecastArm | CreateLegacyArm | CreateRollforwardArm | CreateScheduleArm, idempotency_key: None | str | Unset = UNSET, ) -> ErrorResponse | OperationEnvelopeInformationBlockEnvelope | None: """Create Information Block @@ -184,8 +187,8 @@ def sync( Args: graph_id (str): idempotency_key (None | str | Unset): - body (CreateLegacyArm | CreateRollforwardArm | CreateScheduleArm): Create an Information - Block. The body is a discriminated union on + body (CreateForecastArm | CreateLegacyArm | CreateRollforwardArm | CreateScheduleArm): + Create an Information Block. The body is a discriminated union on `block_type`: pick the arm matching the block type you want to create. The schedule arm carries a fully typed payload; statement and metric arms accept an untyped payload but currently return HTTP @@ -212,7 +215,7 @@ async def asyncio_detailed( graph_id: str, *, client: AuthenticatedClient, - body: CreateLegacyArm | CreateRollforwardArm | CreateScheduleArm, + body: CreateForecastArm | CreateLegacyArm | CreateRollforwardArm | CreateScheduleArm, idempotency_key: None | str | Unset = UNSET, ) -> Response[ErrorResponse | OperationEnvelopeInformationBlockEnvelope]: """Create Information Block @@ -230,8 +233,8 @@ async def asyncio_detailed( Args: graph_id (str): idempotency_key (None | str | Unset): - body (CreateLegacyArm | CreateRollforwardArm | CreateScheduleArm): Create an Information - Block. The body is a discriminated union on + body (CreateForecastArm | CreateLegacyArm | CreateRollforwardArm | CreateScheduleArm): + Create an Information Block. The body is a discriminated union on `block_type`: pick the arm matching the block type you want to create. The schedule arm carries a fully typed payload; statement and metric arms accept an untyped payload but currently return HTTP @@ -261,7 +264,7 @@ async def asyncio( graph_id: str, *, client: AuthenticatedClient, - body: CreateLegacyArm | CreateRollforwardArm | CreateScheduleArm, + body: CreateForecastArm | CreateLegacyArm | CreateRollforwardArm | CreateScheduleArm, idempotency_key: None | str | Unset = UNSET, ) -> ErrorResponse | OperationEnvelopeInformationBlockEnvelope | None: """Create Information Block @@ -279,8 +282,8 @@ async def asyncio( Args: graph_id (str): idempotency_key (None | str | Unset): - body (CreateLegacyArm | CreateRollforwardArm | CreateScheduleArm): Create an Information - Block. The body is a discriminated union on + body (CreateForecastArm | CreateLegacyArm | CreateRollforwardArm | CreateScheduleArm): + Create an Information Block. The body is a discriminated union on `block_type`: pick the arm matching the block type you want to create. The schedule arm carries a fully typed payload; statement and metric arms accept an untyped payload but currently return HTTP diff --git a/robosystems_client/api/extensions_robo_ledger/delete_information_block.py b/robosystems_client/api/extensions_robo_ledger/delete_information_block.py index 6bd261b..3d41c11 100644 --- a/robosystems_client/api/extensions_robo_ledger/delete_information_block.py +++ b/robosystems_client/api/extensions_robo_ledger/delete_information_block.py @@ -6,6 +6,7 @@ from ... import errors from ...client import AuthenticatedClient, Client +from ...models.delete_forecast_arm import DeleteForecastArm from ...models.delete_legacy_arm import DeleteLegacyArm from ...models.delete_rollforward_arm import DeleteRollforwardArm from ...models.delete_schedule_arm import DeleteScheduleArm @@ -19,7 +20,7 @@ def _get_kwargs( graph_id: str, *, - body: DeleteLegacyArm | DeleteRollforwardArm | DeleteScheduleArm, + body: DeleteForecastArm | DeleteLegacyArm | DeleteRollforwardArm | DeleteScheduleArm, idempotency_key: None | str | Unset = UNSET, ) -> dict[str, Any]: headers: dict[str, Any] = {} @@ -37,6 +38,8 @@ def _get_kwargs( _kwargs["json"] = body.to_dict() elif isinstance(body, DeleteRollforwardArm): _kwargs["json"] = body.to_dict() + elif isinstance(body, DeleteForecastArm): + _kwargs["json"] = body.to_dict() else: _kwargs["json"] = body.to_dict() @@ -117,7 +120,7 @@ def sync_detailed( graph_id: str, *, client: AuthenticatedClient, - body: DeleteLegacyArm | DeleteRollforwardArm | DeleteScheduleArm, + body: DeleteForecastArm | DeleteLegacyArm | DeleteRollforwardArm | DeleteScheduleArm, idempotency_key: None | str | Unset = UNSET, ) -> Response[ErrorResponse | OperationEnvelopeDeleteInformationBlockResponse]: """Delete Information Block @@ -132,8 +135,8 @@ def sync_detailed( Args: graph_id (str): idempotency_key (None | str | Unset): - body (DeleteLegacyArm | DeleteRollforwardArm | DeleteScheduleArm): Delete an Information - Block. The body is a discriminated union on + body (DeleteForecastArm | DeleteLegacyArm | DeleteRollforwardArm | DeleteScheduleArm): + Delete an Information Block. The body is a discriminated union on `block_type` mirroring `CreateInformationBlockRequest`. The schedule arm carries a fully typed delete payload; statement and metric arms return HTTP 501. @@ -163,7 +166,7 @@ def sync( graph_id: str, *, client: AuthenticatedClient, - body: DeleteLegacyArm | DeleteRollforwardArm | DeleteScheduleArm, + body: DeleteForecastArm | DeleteLegacyArm | DeleteRollforwardArm | DeleteScheduleArm, idempotency_key: None | str | Unset = UNSET, ) -> ErrorResponse | OperationEnvelopeDeleteInformationBlockResponse | None: """Delete Information Block @@ -178,8 +181,8 @@ def sync( Args: graph_id (str): idempotency_key (None | str | Unset): - body (DeleteLegacyArm | DeleteRollforwardArm | DeleteScheduleArm): Delete an Information - Block. The body is a discriminated union on + body (DeleteForecastArm | DeleteLegacyArm | DeleteRollforwardArm | DeleteScheduleArm): + Delete an Information Block. The body is a discriminated union on `block_type` mirroring `CreateInformationBlockRequest`. The schedule arm carries a fully typed delete payload; statement and metric arms return HTTP 501. @@ -204,7 +207,7 @@ async def asyncio_detailed( graph_id: str, *, client: AuthenticatedClient, - body: DeleteLegacyArm | DeleteRollforwardArm | DeleteScheduleArm, + body: DeleteForecastArm | DeleteLegacyArm | DeleteRollforwardArm | DeleteScheduleArm, idempotency_key: None | str | Unset = UNSET, ) -> Response[ErrorResponse | OperationEnvelopeDeleteInformationBlockResponse]: """Delete Information Block @@ -219,8 +222,8 @@ async def asyncio_detailed( Args: graph_id (str): idempotency_key (None | str | Unset): - body (DeleteLegacyArm | DeleteRollforwardArm | DeleteScheduleArm): Delete an Information - Block. The body is a discriminated union on + body (DeleteForecastArm | DeleteLegacyArm | DeleteRollforwardArm | DeleteScheduleArm): + Delete an Information Block. The body is a discriminated union on `block_type` mirroring `CreateInformationBlockRequest`. The schedule arm carries a fully typed delete payload; statement and metric arms return HTTP 501. @@ -248,7 +251,7 @@ async def asyncio( graph_id: str, *, client: AuthenticatedClient, - body: DeleteLegacyArm | DeleteRollforwardArm | DeleteScheduleArm, + body: DeleteForecastArm | DeleteLegacyArm | DeleteRollforwardArm | DeleteScheduleArm, idempotency_key: None | str | Unset = UNSET, ) -> ErrorResponse | OperationEnvelopeDeleteInformationBlockResponse | None: """Delete Information Block @@ -263,8 +266,8 @@ async def asyncio( Args: graph_id (str): idempotency_key (None | str | Unset): - body (DeleteLegacyArm | DeleteRollforwardArm | DeleteScheduleArm): Delete an Information - Block. The body is a discriminated union on + body (DeleteForecastArm | DeleteLegacyArm | DeleteRollforwardArm | DeleteScheduleArm): + Delete an Information Block. The body is a discriminated union on `block_type` mirroring `CreateInformationBlockRequest`. The schedule arm carries a fully typed delete payload; statement and metric arms return HTTP 501. diff --git a/robosystems_client/api/extensions_robo_ledger/update_information_block.py b/robosystems_client/api/extensions_robo_ledger/update_information_block.py index c2aa250..3e5a04f 100644 --- a/robosystems_client/api/extensions_robo_ledger/update_information_block.py +++ b/robosystems_client/api/extensions_robo_ledger/update_information_block.py @@ -10,6 +10,7 @@ from ...models.operation_envelope_information_block_envelope import ( OperationEnvelopeInformationBlockEnvelope, ) +from ...models.update_forecast_arm import UpdateForecastArm from ...models.update_legacy_arm import UpdateLegacyArm from ...models.update_rollforward_arm import UpdateRollforwardArm from ...models.update_schedule_arm import UpdateScheduleArm @@ -19,7 +20,7 @@ def _get_kwargs( graph_id: str, *, - body: UpdateLegacyArm | UpdateRollforwardArm | UpdateScheduleArm, + body: UpdateForecastArm | UpdateLegacyArm | UpdateRollforwardArm | UpdateScheduleArm, idempotency_key: None | str | Unset = UNSET, ) -> dict[str, Any]: headers: dict[str, Any] = {} @@ -37,6 +38,8 @@ def _get_kwargs( _kwargs["json"] = body.to_dict() elif isinstance(body, UpdateRollforwardArm): _kwargs["json"] = body.to_dict() + elif isinstance(body, UpdateForecastArm): + _kwargs["json"] = body.to_dict() else: _kwargs["json"] = body.to_dict() @@ -115,7 +118,7 @@ def sync_detailed( graph_id: str, *, client: AuthenticatedClient, - body: UpdateLegacyArm | UpdateRollforwardArm | UpdateScheduleArm, + body: UpdateForecastArm | UpdateLegacyArm | UpdateRollforwardArm | UpdateScheduleArm, idempotency_key: None | str | Unset = UNSET, ) -> Response[ErrorResponse | OperationEnvelopeInformationBlockEnvelope]: """Update Information Block @@ -130,8 +133,8 @@ def sync_detailed( Args: graph_id (str): idempotency_key (None | str | Unset): - body (UpdateLegacyArm | UpdateRollforwardArm | UpdateScheduleArm): Update an Information - Block. The body is a discriminated union on + body (UpdateForecastArm | UpdateLegacyArm | UpdateRollforwardArm | UpdateScheduleArm): + Update an Information Block. The body is a discriminated union on `block_type` mirroring `CreateInformationBlockRequest`. The schedule arm carries a fully typed update payload; statement and metric arms return HTTP 501 (statements are library-seeded; metric updates are @@ -162,7 +165,7 @@ def sync( graph_id: str, *, client: AuthenticatedClient, - body: UpdateLegacyArm | UpdateRollforwardArm | UpdateScheduleArm, + body: UpdateForecastArm | UpdateLegacyArm | UpdateRollforwardArm | UpdateScheduleArm, idempotency_key: None | str | Unset = UNSET, ) -> ErrorResponse | OperationEnvelopeInformationBlockEnvelope | None: """Update Information Block @@ -177,8 +180,8 @@ def sync( Args: graph_id (str): idempotency_key (None | str | Unset): - body (UpdateLegacyArm | UpdateRollforwardArm | UpdateScheduleArm): Update an Information - Block. The body is a discriminated union on + body (UpdateForecastArm | UpdateLegacyArm | UpdateRollforwardArm | UpdateScheduleArm): + Update an Information Block. The body is a discriminated union on `block_type` mirroring `CreateInformationBlockRequest`. The schedule arm carries a fully typed update payload; statement and metric arms return HTTP 501 (statements are library-seeded; metric updates are @@ -204,7 +207,7 @@ async def asyncio_detailed( graph_id: str, *, client: AuthenticatedClient, - body: UpdateLegacyArm | UpdateRollforwardArm | UpdateScheduleArm, + body: UpdateForecastArm | UpdateLegacyArm | UpdateRollforwardArm | UpdateScheduleArm, idempotency_key: None | str | Unset = UNSET, ) -> Response[ErrorResponse | OperationEnvelopeInformationBlockEnvelope]: """Update Information Block @@ -219,8 +222,8 @@ async def asyncio_detailed( Args: graph_id (str): idempotency_key (None | str | Unset): - body (UpdateLegacyArm | UpdateRollforwardArm | UpdateScheduleArm): Update an Information - Block. The body is a discriminated union on + body (UpdateForecastArm | UpdateLegacyArm | UpdateRollforwardArm | UpdateScheduleArm): + Update an Information Block. The body is a discriminated union on `block_type` mirroring `CreateInformationBlockRequest`. The schedule arm carries a fully typed update payload; statement and metric arms return HTTP 501 (statements are library-seeded; metric updates are @@ -249,7 +252,7 @@ async def asyncio( graph_id: str, *, client: AuthenticatedClient, - body: UpdateLegacyArm | UpdateRollforwardArm | UpdateScheduleArm, + body: UpdateForecastArm | UpdateLegacyArm | UpdateRollforwardArm | UpdateScheduleArm, idempotency_key: None | str | Unset = UNSET, ) -> ErrorResponse | OperationEnvelopeInformationBlockEnvelope | None: """Update Information Block @@ -264,8 +267,8 @@ async def asyncio( Args: graph_id (str): idempotency_key (None | str | Unset): - body (UpdateLegacyArm | UpdateRollforwardArm | UpdateScheduleArm): Update an Information - Block. The body is a discriminated union on + body (UpdateForecastArm | UpdateLegacyArm | UpdateRollforwardArm | UpdateScheduleArm): + Update an Information Block. The body is a discriminated union on `block_type` mirroring `CreateInformationBlockRequest`. The schedule arm carries a fully typed update payload; statement and metric arms return HTTP 501 (statements are library-seeded; metric updates are diff --git a/robosystems_client/models/__init__.py b/robosystems_client/models/__init__.py index 35e9db6..55d035c 100644 --- a/robosystems_client/models/__init__.py +++ b/robosystems_client/models/__init__.py @@ -46,6 +46,8 @@ from .close_period_response_rule_summary_type_0 import ( ClosePeriodResponseRuleSummaryType0, ) +from .compute_forecast_request import ComputeForecastRequest +from .compute_forecast_response import ComputeForecastResponse from .compute_metrics_request import ComputeMetricsRequest from .compute_metrics_response import ComputeMetricsResponse from .computed_metric_lite import ComputedMetricLite @@ -85,6 +87,9 @@ ) from .create_event_handler_request_metadata import CreateEventHandlerRequestMetadata from .create_event_handler_request_origin import CreateEventHandlerRequestOrigin +from .create_forecast_arm import CreateForecastArm +from .create_forecast_request import CreateForecastRequest +from .create_forecast_request_scenario_kind import CreateForecastRequestScenarioKind from .create_graph_request import CreateGraphRequest from .create_legacy_arm import CreateLegacyArm from .create_legacy_arm_block_type import CreateLegacyArmBlockType @@ -133,6 +138,8 @@ from .database_storage_entry import DatabaseStorageEntry from .delete_document_op import DeleteDocumentOp from .delete_file_op import DeleteFileOp +from .delete_forecast_arm import DeleteForecastArm +from .delete_forecast_request import DeleteForecastRequest from .delete_graph_op import DeleteGraphOp from .delete_information_block_response import DeleteInformationBlockResponse from .delete_journal_entry_request import DeleteJournalEntryRequest @@ -205,6 +212,9 @@ from .financial_statement_analysis_request import FinancialStatementAnalysisRequest from .fiscal_calendar_response import FiscalCalendarResponse from .fiscal_period_summary import FiscalPeriodSummary +from .forecast_mechanics import ForecastMechanics +from .forecast_mechanics_scenario_kind import ForecastMechanicsScenarioKind +from .forecast_month_lite import ForecastMonthLite from .forget_op import ForgetOp from .forgot_password_request import ForgotPasswordRequest from .forgot_password_response_forgotpassword import ( @@ -265,6 +275,12 @@ from .ledger_agent_response import LedgerAgentResponse from .ledger_agent_response_address_type_0 import LedgerAgentResponseAddressType0 from .ledger_entity_response import LedgerEntityResponse +from .lever_assertion_lite import LeverAssertionLite +from .lever_assertion_lite_values_by_period import LeverAssertionLiteValuesByPeriod +from .lever_assertion_request import LeverAssertionRequest +from .lever_assertion_request_values_by_period_type_0 import ( + LeverAssertionRequestValuesByPeriodType0, +) from .line_item_metadata_predicate import LineItemMetadataPredicate from .link_entity_taxonomy_request import LinkEntityTaxonomyRequest from .link_entity_taxonomy_request_basis import LinkEntityTaxonomyRequestBasis @@ -323,6 +339,12 @@ from .operation_envelope_close_period_response_status import ( OperationEnvelopeClosePeriodResponseStatus, ) +from .operation_envelope_compute_forecast_response import ( + OperationEnvelopeComputeForecastResponse, +) +from .operation_envelope_compute_forecast_response_status import ( + OperationEnvelopeComputeForecastResponseStatus, +) from .operation_envelope_compute_metrics_response import ( OperationEnvelopeComputeMetricsResponse, ) @@ -606,6 +628,7 @@ from .share_report_operation import ShareReportOperation from .share_report_response import ShareReportResponse from .share_result_item import ShareResultItem +from .skipped_forecast_lite import SkippedForecastLite from .skipped_metric_lite import SkippedMetricLite from .sql_statement_request import SqlStatementRequest from .sql_statement_response import SqlStatementResponse @@ -707,6 +730,11 @@ from .update_event_handler_request_metadata_patch import ( UpdateEventHandlerRequestMetadataPatch, ) +from .update_forecast_arm import UpdateForecastArm +from .update_forecast_request import UpdateForecastRequest +from .update_forecast_request_scenario_kind_type_0 import ( + UpdateForecastRequestScenarioKindType0, +) from .update_journal_entry_request import UpdateJournalEntryRequest from .update_journal_entry_request_type_type_0 import UpdateJournalEntryRequestTypeType0 from .update_legacy_arm import UpdateLegacyArm @@ -789,6 +817,8 @@ "ClosePeriodResponse", "ClosePeriodResponseRuleSummaryType0", "ComputedMetricLite", + "ComputeForecastRequest", + "ComputeForecastResponse", "ComputeMetricsRequest", "ComputeMetricsResponse", "ConnectionLite", @@ -819,6 +849,9 @@ "CreateEventHandlerRequestMatchMetadataExpressionType0", "CreateEventHandlerRequestMetadata", "CreateEventHandlerRequestOrigin", + "CreateForecastArm", + "CreateForecastRequest", + "CreateForecastRequestScenarioKind", "CreateGraphRequest", "CreateLegacyArm", "CreateLegacyArmBlockType", @@ -857,6 +890,8 @@ "DatabaseStorageEntry", "DeleteDocumentOp", "DeleteFileOp", + "DeleteForecastArm", + "DeleteForecastRequest", "DeleteGraphOp", "DeleteInformationBlockResponse", "DeleteJournalEntryRequest", @@ -919,6 +954,9 @@ "FinancialStatementAnalysisRequest", "FiscalCalendarResponse", "FiscalPeriodSummary", + "ForecastMechanics", + "ForecastMechanicsScenarioKind", + "ForecastMonthLite", "ForgetOp", "ForgotPasswordRequest", "ForgotPasswordResponseForgotpassword", @@ -967,6 +1005,10 @@ "LedgerAgentResponse", "LedgerAgentResponseAddressType0", "LedgerEntityResponse", + "LeverAssertionLite", + "LeverAssertionLiteValuesByPeriod", + "LeverAssertionRequest", + "LeverAssertionRequestValuesByPeriodType0", "LineItemMetadataPredicate", "LinkEntityTaxonomyRequest", "LinkEntityTaxonomyRequestBasis", @@ -1005,6 +1047,8 @@ "OperationEnvelopeChangeReportingStyleResponseStatus", "OperationEnvelopeClosePeriodResponse", "OperationEnvelopeClosePeriodResponseStatus", + "OperationEnvelopeComputeForecastResponse", + "OperationEnvelopeComputeForecastResponseStatus", "OperationEnvelopeComputeMetricsResponse", "OperationEnvelopeComputeMetricsResponseStatus", "OperationEnvelopeDeleteInformationBlockResponse", @@ -1176,6 +1220,7 @@ "ShareReportOperation", "ShareReportResponse", "ShareResultItem", + "SkippedForecastLite", "SkippedMetricLite", "SqlStatementRequest", "SqlStatementResponse", @@ -1245,6 +1290,9 @@ "UpdateEventHandlerRequest", "UpdateEventHandlerRequestMatchMetadataExpressionType0", "UpdateEventHandlerRequestMetadataPatch", + "UpdateForecastArm", + "UpdateForecastRequest", + "UpdateForecastRequestScenarioKindType0", "UpdateJournalEntryRequest", "UpdateJournalEntryRequestTypeType0", "UpdateLegacyArm", diff --git a/robosystems_client/models/artifact_response.py b/robosystems_client/models/artifact_response.py index 91f8513..13f9b16 100644 --- a/robosystems_client/models/artifact_response.py +++ b/robosystems_client/models/artifact_response.py @@ -10,6 +10,7 @@ if TYPE_CHECKING: from ..models.artifact_response_template_type_0 import ArtifactResponseTemplateType0 + from ..models.forecast_mechanics import ForecastMechanics from ..models.metric_mechanics import MetricMechanics from ..models.rollforward_mechanics import RollforwardMechanics from ..models.schedule_mechanics import ScheduleMechanics @@ -24,7 +25,7 @@ class ArtifactResponse: """The block's producible-artifact envelope — topic, template, mechanics. Attributes: - mechanics (MetricMechanics | RollforwardMechanics | ScheduleMechanics | StatementMechanics): + mechanics (ForecastMechanics | MetricMechanics | RollforwardMechanics | ScheduleMechanics | StatementMechanics): topic (None | str | Unset): Structure.description — the block's human-readable topic. renderer_note (None | str | Unset): e.g. 'in thousands', 'except per share'. template (ArtifactResponseTemplateType0 | None | Unset): Reusable layout (ordering, subtotals, styling) when @@ -33,7 +34,11 @@ class ArtifactResponse: """ mechanics: ( - MetricMechanics | RollforwardMechanics | ScheduleMechanics | StatementMechanics + ForecastMechanics + | MetricMechanics + | RollforwardMechanics + | ScheduleMechanics + | StatementMechanics ) topic: None | str | Unset = UNSET renderer_note: None | str | Unset = UNSET @@ -43,6 +48,7 @@ class ArtifactResponse: def to_dict(self) -> dict[str, Any]: from ..models.artifact_response_template_type_0 import ArtifactResponseTemplateType0 from ..models.metric_mechanics import MetricMechanics + from ..models.rollforward_mechanics import RollforwardMechanics from ..models.schedule_mechanics import ScheduleMechanics from ..models.statement_mechanics import StatementMechanics @@ -53,6 +59,8 @@ def to_dict(self) -> dict[str, Any]: mechanics = self.mechanics.to_dict() elif isinstance(self.mechanics, MetricMechanics): mechanics = self.mechanics.to_dict() + elif isinstance(self.mechanics, RollforwardMechanics): + mechanics = self.mechanics.to_dict() else: mechanics = self.mechanics.to_dict() @@ -95,6 +103,7 @@ def to_dict(self) -> dict[str, Any]: @classmethod def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: from ..models.artifact_response_template_type_0 import ArtifactResponseTemplateType0 + from ..models.forecast_mechanics import ForecastMechanics from ..models.metric_mechanics import MetricMechanics from ..models.rollforward_mechanics import RollforwardMechanics from ..models.schedule_mechanics import ScheduleMechanics @@ -105,7 +114,11 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: def _parse_mechanics( data: object, ) -> ( - MetricMechanics | RollforwardMechanics | ScheduleMechanics | StatementMechanics + ForecastMechanics + | MetricMechanics + | RollforwardMechanics + | ScheduleMechanics + | StatementMechanics ): try: if not isinstance(data, dict): @@ -131,11 +144,19 @@ def _parse_mechanics( return mechanics_type_2 except (TypeError, ValueError, AttributeError, KeyError): pass + try: + if not isinstance(data, dict): + raise TypeError() + mechanics_type_3 = RollforwardMechanics.from_dict(data) + + return mechanics_type_3 + except (TypeError, ValueError, AttributeError, KeyError): + pass if not isinstance(data, dict): raise TypeError() - mechanics_type_3 = RollforwardMechanics.from_dict(data) + mechanics_type_4 = ForecastMechanics.from_dict(data) - return mechanics_type_3 + return mechanics_type_4 mechanics = _parse_mechanics(d.pop("mechanics")) diff --git a/robosystems_client/models/compute_forecast_request.py b/robosystems_client/models/compute_forecast_request.py new file mode 100644 index 0000000..239aa98 --- /dev/null +++ b/robosystems_client/models/compute_forecast_request.py @@ -0,0 +1,115 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="ComputeForecastRequest") + + +@_attrs_define +class ComputeForecastRequest: + """Request body for the ``compute-forecast`` operation. + + Walks the scenario's driver cascade month-by-month forward from the + forecast block's ``base_period``: lever-driven Derive rules in + dependency order, carry-forward for unmodeled IS lines, calc-DAG + subtotals — upserting one scenario IS FactSet (+ a working-capital BS + set) per forward month, all keyed by the forecast block's + ``scenario_id``. Re-running replaces each month's values (the + compute-metrics drift semantics). Deterministic and non-AI — no + credits consumed. + + Attributes: + structure_id (str): Forecast block structure (block_type='forecast') to compute. + months (int | None | Unset): Forward months to compute — defaults to the block's full horizon_months; must not + exceed it (lever assertions don't extend past the horizon). + entity_id (None | str | Unset): Entity to compute for. Defaults to the lever FactSet's entity (the entity the + scenario was authored against). + """ + + structure_id: str + months: int | None | Unset = UNSET + entity_id: None | str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + structure_id = self.structure_id + + months: int | None | Unset + if isinstance(self.months, Unset): + months = UNSET + else: + months = self.months + + entity_id: None | str | Unset + if isinstance(self.entity_id, Unset): + entity_id = UNSET + else: + entity_id = self.entity_id + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "structure_id": structure_id, + } + ) + if months is not UNSET: + field_dict["months"] = months + if entity_id is not UNSET: + field_dict["entity_id"] = entity_id + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + structure_id = d.pop("structure_id") + + def _parse_months(data: object) -> int | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(int | None | Unset, data) + + months = _parse_months(d.pop("months", UNSET)) + + def _parse_entity_id(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + entity_id = _parse_entity_id(d.pop("entity_id", UNSET)) + + compute_forecast_request = cls( + structure_id=structure_id, + months=months, + entity_id=entity_id, + ) + + compute_forecast_request.additional_properties = d + return compute_forecast_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/robosystems_client/models/compute_forecast_response.py b/robosystems_client/models/compute_forecast_response.py new file mode 100644 index 0000000..4433e0a --- /dev/null +++ b/robosystems_client/models/compute_forecast_response.py @@ -0,0 +1,146 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.forecast_month_lite import ForecastMonthLite + from ..models.skipped_forecast_lite import SkippedForecastLite + + +T = TypeVar("T", bound="ComputeForecastResponse") + + +@_attrs_define +class ComputeForecastResponse: + """Response for the ``compute-forecast`` operation. + + Attributes: + structure_id (str): + scenario_id (str): The scenario key every emitted FactSet carries — the forecast block's own structure id. + entity_id (str): + base_period (str): Seed month the walk projected from. + months (int): Forward months requested. + months_computed (list[ForecastMonthLite] | Unset): + skipped (list[SkippedForecastLite] | Unset): + """ + + structure_id: str + scenario_id: str + entity_id: str + base_period: str + months: int + months_computed: list[ForecastMonthLite] | Unset = UNSET + skipped: list[SkippedForecastLite] | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + structure_id = self.structure_id + + scenario_id = self.scenario_id + + entity_id = self.entity_id + + base_period = self.base_period + + months = self.months + + months_computed: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.months_computed, Unset): + months_computed = [] + for months_computed_item_data in self.months_computed: + months_computed_item = months_computed_item_data.to_dict() + months_computed.append(months_computed_item) + + skipped: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.skipped, Unset): + skipped = [] + for skipped_item_data in self.skipped: + skipped_item = skipped_item_data.to_dict() + skipped.append(skipped_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "structure_id": structure_id, + "scenario_id": scenario_id, + "entity_id": entity_id, + "base_period": base_period, + "months": months, + } + ) + if months_computed is not UNSET: + field_dict["months_computed"] = months_computed + if skipped is not UNSET: + field_dict["skipped"] = skipped + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.forecast_month_lite import ForecastMonthLite + from ..models.skipped_forecast_lite import SkippedForecastLite + + d = dict(src_dict) + structure_id = d.pop("structure_id") + + scenario_id = d.pop("scenario_id") + + entity_id = d.pop("entity_id") + + base_period = d.pop("base_period") + + months = d.pop("months") + + _months_computed = d.pop("months_computed", UNSET) + months_computed: list[ForecastMonthLite] | Unset = UNSET + if _months_computed is not UNSET: + months_computed = [] + for months_computed_item_data in _months_computed: + months_computed_item = ForecastMonthLite.from_dict(months_computed_item_data) + + months_computed.append(months_computed_item) + + _skipped = d.pop("skipped", UNSET) + skipped: list[SkippedForecastLite] | Unset = UNSET + if _skipped is not UNSET: + skipped = [] + for skipped_item_data in _skipped: + skipped_item = SkippedForecastLite.from_dict(skipped_item_data) + + skipped.append(skipped_item) + + compute_forecast_response = cls( + structure_id=structure_id, + scenario_id=scenario_id, + entity_id=entity_id, + base_period=base_period, + months=months, + months_computed=months_computed, + skipped=skipped, + ) + + compute_forecast_response.additional_properties = d + return compute_forecast_response + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/robosystems_client/models/compute_metrics_request.py b/robosystems_client/models/compute_metrics_request.py index f73b59b..6c2f72f 100644 --- a/robosystems_client/models/compute_metrics_request.py +++ b/robosystems_client/models/compute_metrics_request.py @@ -31,12 +31,17 @@ class ComputeMetricsRequest: FactSet's period_start. entity_id (None | str | Unset): Entity to compute for. Defaults to the graph's earliest-created entity (the primary entity for single-entity graphs). + scenario_id (None | str | Unset): Compute on a scenario slice: operands bind that scenario's facts (actuals as + the fallback across the seam) and the standing metric set is stamped with the scenario. None (the default) + computes actuals only. Pass a forecast block's structure id after compute-forecast to extend the metric series + into its forward months. """ structure_id: str period_end: datetime.date period_start: datetime.date | None | Unset = UNSET entity_id: None | str | Unset = UNSET + scenario_id: None | str | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -58,6 +63,12 @@ def to_dict(self) -> dict[str, Any]: else: entity_id = self.entity_id + scenario_id: None | str | Unset + if isinstance(self.scenario_id, Unset): + scenario_id = UNSET + else: + scenario_id = self.scenario_id + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -70,6 +81,8 @@ def to_dict(self) -> dict[str, Any]: field_dict["period_start"] = period_start if entity_id is not UNSET: field_dict["entity_id"] = entity_id + if scenario_id is not UNSET: + field_dict["scenario_id"] = scenario_id return field_dict @@ -106,11 +119,21 @@ def _parse_entity_id(data: object) -> None | str | Unset: entity_id = _parse_entity_id(d.pop("entity_id", UNSET)) + def _parse_scenario_id(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + scenario_id = _parse_scenario_id(d.pop("scenario_id", UNSET)) + compute_metrics_request = cls( structure_id=structure_id, period_end=period_end, period_start=period_start, entity_id=entity_id, + scenario_id=scenario_id, ) compute_metrics_request.additional_properties = d diff --git a/robosystems_client/models/create_forecast_arm.py b/robosystems_client/models/create_forecast_arm.py new file mode 100644 index 0000000..54b2c4d --- /dev/null +++ b/robosystems_client/models/create_forecast_arm.py @@ -0,0 +1,88 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Literal, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.create_forecast_request import CreateForecastRequest + + +T = TypeVar("T", bound="CreateForecastArm") + + +@_attrs_define +class CreateForecastArm: + """Create-information-block body for ``block_type="forecast"``. + + Carries a typed forecast payload — the authored scenario container: + scenario identity, horizon, base period, lever assertions on + ``rs-driver:*`` catalog elements. Run ``compute-forecast`` after + creating to derive the forward months. + + Attributes: + block_type (Literal['forecast']): Discriminator value selecting this arm. + payload (CreateForecastRequest): Create a forecast block — the authored scenario container. + + ``base_period`` defaults to the fiscal calendar's + ``closed_through_period`` (else the newest actual report month) — + the walk projects forward from the last closed actuals. The resolved + value is stored in the mechanics so recompute is deterministic. + """ + + block_type: Literal["forecast"] + payload: CreateForecastRequest + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + block_type = self.block_type + + payload = self.payload.to_dict() + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "block_type": block_type, + "payload": payload, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.create_forecast_request import CreateForecastRequest + + d = dict(src_dict) + block_type = cast(Literal["forecast"], d.pop("block_type")) + if block_type != "forecast": + raise ValueError(f"block_type must match const 'forecast', got '{block_type}'") + + payload = CreateForecastRequest.from_dict(d.pop("payload")) + + create_forecast_arm = cls( + block_type=block_type, + payload=payload, + ) + + create_forecast_arm.additional_properties = d + return create_forecast_arm + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/robosystems_client/models/create_forecast_request.py b/robosystems_client/models/create_forecast_request.py new file mode 100644 index 0000000..6d03a39 --- /dev/null +++ b/robosystems_client/models/create_forecast_request.py @@ -0,0 +1,166 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.create_forecast_request_scenario_kind import ( + CreateForecastRequestScenarioKind, +) +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.lever_assertion_request import LeverAssertionRequest + + +T = TypeVar("T", bound="CreateForecastRequest") + + +@_attrs_define +class CreateForecastRequest: + """Create a forecast block — the authored scenario container. + + ``base_period`` defaults to the fiscal calendar's + ``closed_through_period`` (else the newest actual report month) — + the walk projects forward from the last closed actuals. The resolved + value is stored in the mechanics so recompute is deterministic. + + Attributes: + name (str): Human-readable scenario name. + levers (list[LeverAssertionRequest]): Lever assertions — at least one. + scenario_kind (CreateForecastRequestScenarioKind | Unset): What kind of scenario this is — metadata for + display/filtering, not machinery. All kinds compute identically. Default: + CreateForecastRequestScenarioKind.FORECAST. + horizon_months (int | Unset): Forward months to project past the base period. Default: 12. + base_period (None | str | Unset): Seed month (``YYYY-MM``) the walk projects forward from. Defaults to the + fiscal calendar's closed-through period, else the newest actual report month. Resolved and stored at create + time. + entity_id (None | str | Unset): Entity the scenario belongs to. Defaults to the graph's earliest-created entity + (single-entity convention). + """ + + name: str + levers: list[LeverAssertionRequest] + scenario_kind: CreateForecastRequestScenarioKind | Unset = ( + CreateForecastRequestScenarioKind.FORECAST + ) + horizon_months: int | Unset = 12 + base_period: None | str | Unset = UNSET + entity_id: None | str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + name = self.name + + levers = [] + for levers_item_data in self.levers: + levers_item = levers_item_data.to_dict() + levers.append(levers_item) + + scenario_kind: str | Unset = UNSET + if not isinstance(self.scenario_kind, Unset): + scenario_kind = self.scenario_kind.value + + horizon_months = self.horizon_months + + base_period: None | str | Unset + if isinstance(self.base_period, Unset): + base_period = UNSET + else: + base_period = self.base_period + + entity_id: None | str | Unset + if isinstance(self.entity_id, Unset): + entity_id = UNSET + else: + entity_id = self.entity_id + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "name": name, + "levers": levers, + } + ) + if scenario_kind is not UNSET: + field_dict["scenario_kind"] = scenario_kind + if horizon_months is not UNSET: + field_dict["horizon_months"] = horizon_months + if base_period is not UNSET: + field_dict["base_period"] = base_period + if entity_id is not UNSET: + field_dict["entity_id"] = entity_id + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.lever_assertion_request import LeverAssertionRequest + + d = dict(src_dict) + name = d.pop("name") + + levers = [] + _levers = d.pop("levers") + for levers_item_data in _levers: + levers_item = LeverAssertionRequest.from_dict(levers_item_data) + + levers.append(levers_item) + + _scenario_kind = d.pop("scenario_kind", UNSET) + scenario_kind: CreateForecastRequestScenarioKind | Unset + if isinstance(_scenario_kind, Unset): + scenario_kind = UNSET + else: + scenario_kind = CreateForecastRequestScenarioKind(_scenario_kind) + + horizon_months = d.pop("horizon_months", UNSET) + + def _parse_base_period(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + base_period = _parse_base_period(d.pop("base_period", UNSET)) + + def _parse_entity_id(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + entity_id = _parse_entity_id(d.pop("entity_id", UNSET)) + + create_forecast_request = cls( + name=name, + levers=levers, + scenario_kind=scenario_kind, + horizon_months=horizon_months, + base_period=base_period, + entity_id=entity_id, + ) + + create_forecast_request.additional_properties = d + return create_forecast_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/robosystems_client/models/create_forecast_request_scenario_kind.py b/robosystems_client/models/create_forecast_request_scenario_kind.py new file mode 100644 index 0000000..3466f8a --- /dev/null +++ b/robosystems_client/models/create_forecast_request_scenario_kind.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class CreateForecastRequestScenarioKind(str, Enum): + BUDGET = "budget" + FORECAST = "forecast" + PROJECTION = "projection" + + def __str__(self) -> str: + return str(self.value) diff --git a/robosystems_client/models/delete_forecast_arm.py b/robosystems_client/models/delete_forecast_arm.py new file mode 100644 index 0000000..9167f35 --- /dev/null +++ b/robosystems_client/models/delete_forecast_arm.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Literal, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.delete_forecast_request import DeleteForecastRequest + + +T = TypeVar("T", bound="DeleteForecastArm") + + +@_attrs_define +class DeleteForecastArm: + """Delete-information-block body for ``block_type="forecast"``. + + Removes the scenario's entire parallel universe — the lever FactSet + and every computed scenario FactSet. Actuals are never touched. + + Attributes: + block_type (Literal['forecast']): Discriminator value selecting this arm. + payload (DeleteForecastRequest): Delete a forecast block. + + Removes the scenario's entire parallel universe: the lever FactSet + AND every computed scenario FactSet (the forward statement/metric + months keyed by this scenario). Actuals are never touched. + """ + + block_type: Literal["forecast"] + payload: DeleteForecastRequest + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + block_type = self.block_type + + payload = self.payload.to_dict() + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "block_type": block_type, + "payload": payload, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.delete_forecast_request import DeleteForecastRequest + + d = dict(src_dict) + block_type = cast(Literal["forecast"], d.pop("block_type")) + if block_type != "forecast": + raise ValueError(f"block_type must match const 'forecast', got '{block_type}'") + + payload = DeleteForecastRequest.from_dict(d.pop("payload")) + + delete_forecast_arm = cls( + block_type=block_type, + payload=payload, + ) + + delete_forecast_arm.additional_properties = d + return delete_forecast_arm + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/robosystems_client/models/delete_forecast_request.py b/robosystems_client/models/delete_forecast_request.py new file mode 100644 index 0000000..a4ca479 --- /dev/null +++ b/robosystems_client/models/delete_forecast_request.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="DeleteForecastRequest") + + +@_attrs_define +class DeleteForecastRequest: + """Delete a forecast block. + + Removes the scenario's entire parallel universe: the lever FactSet + AND every computed scenario FactSet (the forward statement/metric + months keyed by this scenario). Actuals are never touched. + + Attributes: + structure_id (str): Structure ID of the forecast block. + """ + + structure_id: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + structure_id = self.structure_id + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "structure_id": structure_id, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + structure_id = d.pop("structure_id") + + delete_forecast_request = cls( + structure_id=structure_id, + ) + + delete_forecast_request.additional_properties = d + return delete_forecast_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/robosystems_client/models/fact_set_lite.py b/robosystems_client/models/fact_set_lite.py index 74ce741..5447612 100644 --- a/robosystems_client/models/fact_set_lite.py +++ b/robosystems_client/models/fact_set_lite.py @@ -28,13 +28,15 @@ class FactSetLite: Attributes: id (str): period_end (datetime.date): - factset_type (str): 'report' | 'schedule' | 'custom' | 'disclosure'. Enum closure enforced by the + factset_type (str): 'report' | 'schedule' | 'custom' | 'disclosure' | 'metric'. Enum closure enforced by the ``public.fact_sets`` CHECK constraint. entity_id (str): structure_id (None | str | Unset): period_start (datetime.date | None | Unset): report_id (None | str | Unset): Back-pointer to the ``reports`` table while ``report_id`` still lives on facts. Drops out once the retirement migration lands. + scenario_id (None | str | Unset): Scenario axis (the forecast engine). NULL = actuals; non-NULL names the owning + forecast block whose parallel universe this set belongs to. provenance (FactSetLiteProvenanceType0 | None | Unset): Typed ``FactProvenance`` descriptor (discriminated on ``origin``: pivot | schedule | derived | asserted) recording how this FactSet's facts were constructed. Surfaced as JSON, mirroring how mechanics is exposed. Null for pre-feature historical FactSets. @@ -47,6 +49,7 @@ class FactSetLite: structure_id: None | str | Unset = UNSET period_start: datetime.date | None | Unset = UNSET report_id: None | str | Unset = UNSET + scenario_id: None | str | Unset = UNSET provenance: FactSetLiteProvenanceType0 | None | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) @@ -81,6 +84,12 @@ def to_dict(self) -> dict[str, Any]: else: report_id = self.report_id + scenario_id: None | str | Unset + if isinstance(self.scenario_id, Unset): + scenario_id = UNSET + else: + scenario_id = self.scenario_id + provenance: dict[str, Any] | None | Unset if isinstance(self.provenance, Unset): provenance = UNSET @@ -105,6 +114,8 @@ def to_dict(self) -> dict[str, Any]: field_dict["period_start"] = period_start if report_id is not UNSET: field_dict["report_id"] = report_id + if scenario_id is not UNSET: + field_dict["scenario_id"] = scenario_id if provenance is not UNSET: field_dict["provenance"] = provenance @@ -158,6 +169,15 @@ def _parse_report_id(data: object) -> None | str | Unset: report_id = _parse_report_id(d.pop("report_id", UNSET)) + def _parse_scenario_id(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + scenario_id = _parse_scenario_id(d.pop("scenario_id", UNSET)) + def _parse_provenance(data: object) -> FactSetLiteProvenanceType0 | None | Unset: if data is None: return data @@ -183,6 +203,7 @@ def _parse_provenance(data: object) -> FactSetLiteProvenanceType0 | None | Unset structure_id=structure_id, period_start=period_start, report_id=report_id, + scenario_id=scenario_id, provenance=provenance, ) diff --git a/robosystems_client/models/forecast_mechanics.py b/robosystems_client/models/forecast_mechanics.py new file mode 100644 index 0000000..96a52ef --- /dev/null +++ b/robosystems_client/models/forecast_mechanics.py @@ -0,0 +1,146 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Literal, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.forecast_mechanics_scenario_kind import ForecastMechanicsScenarioKind +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.lever_assertion_lite import LeverAssertionLite + + +T = TypeVar("T", bound="ForecastMechanics") + + +@_attrs_define +class ForecastMechanics: + """Authored scenario container for ``block_type='forecast'`` (FP&A F-1). + + The block IS the scenario: its structure id is the ``scenario_id`` + every derived forward FactSet carries (NULL = actuals). The authored + surface is exactly this — scenario identity, horizon, base period, + lever assertions; everything downstream is derived by + ``compute-forecast`` (levers → driven rs-gaap anchors via the + rs-driver Derive rules → carry-forward for unmodeled IS lines → + calc-DAG subtotals), landing in the EXISTING statement/metric block + types stamped with the scenario. Reads directly from the typed + ``structures.artifact_mechanics`` JSONB column. + + Attributes: + horizon_months (int): Forward months projected past the base period. + base_period (str): Seed month (``YYYY-MM``) the walk projects forward from — resolved at create time (request → + fiscal calendar closed-through → newest actual report month) and stored so recompute is deterministic. + levers (list[LeverAssertionLite]): Expanded lever assertions (authoring order). + kind (Literal['forecast'] | Unset): Default: 'forecast'. + scenario_kind (ForecastMechanicsScenarioKind | Unset): Scenario kind — display/filter metadata, not machinery. + Default: ForecastMechanicsScenarioKind.FORECAST. + computed_months (int | Unset): Number of forward months with computed scenario FactSets. Runtime state filled at + envelope-build time — 0 until the first compute-forecast run. Default: 0. + """ + + horizon_months: int + base_period: str + levers: list[LeverAssertionLite] + kind: Literal["forecast"] | Unset = "forecast" + scenario_kind: ForecastMechanicsScenarioKind | Unset = ( + ForecastMechanicsScenarioKind.FORECAST + ) + computed_months: int | Unset = 0 + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + horizon_months = self.horizon_months + + base_period = self.base_period + + levers = [] + for levers_item_data in self.levers: + levers_item = levers_item_data.to_dict() + levers.append(levers_item) + + kind = self.kind + + scenario_kind: str | Unset = UNSET + if not isinstance(self.scenario_kind, Unset): + scenario_kind = self.scenario_kind.value + + computed_months = self.computed_months + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "horizon_months": horizon_months, + "base_period": base_period, + "levers": levers, + } + ) + if kind is not UNSET: + field_dict["kind"] = kind + if scenario_kind is not UNSET: + field_dict["scenario_kind"] = scenario_kind + if computed_months is not UNSET: + field_dict["computed_months"] = computed_months + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.lever_assertion_lite import LeverAssertionLite + + d = dict(src_dict) + horizon_months = d.pop("horizon_months") + + base_period = d.pop("base_period") + + levers = [] + _levers = d.pop("levers") + for levers_item_data in _levers: + levers_item = LeverAssertionLite.from_dict(levers_item_data) + + levers.append(levers_item) + + kind = cast(Literal["forecast"] | Unset, d.pop("kind", UNSET)) + if kind != "forecast" and not isinstance(kind, Unset): + raise ValueError(f"kind must match const 'forecast', got '{kind}'") + + _scenario_kind = d.pop("scenario_kind", UNSET) + scenario_kind: ForecastMechanicsScenarioKind | Unset + if isinstance(_scenario_kind, Unset): + scenario_kind = UNSET + else: + scenario_kind = ForecastMechanicsScenarioKind(_scenario_kind) + + computed_months = d.pop("computed_months", UNSET) + + forecast_mechanics = cls( + horizon_months=horizon_months, + base_period=base_period, + levers=levers, + kind=kind, + scenario_kind=scenario_kind, + computed_months=computed_months, + ) + + forecast_mechanics.additional_properties = d + return forecast_mechanics + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/robosystems_client/models/forecast_mechanics_scenario_kind.py b/robosystems_client/models/forecast_mechanics_scenario_kind.py new file mode 100644 index 0000000..cd2ee97 --- /dev/null +++ b/robosystems_client/models/forecast_mechanics_scenario_kind.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class ForecastMechanicsScenarioKind(str, Enum): + BUDGET = "budget" + FORECAST = "forecast" + PROJECTION = "projection" + + def __str__(self) -> str: + return str(self.value) diff --git a/robosystems_client/models/forecast_month_lite.py b/robosystems_client/models/forecast_month_lite.py new file mode 100644 index 0000000..d4c80de --- /dev/null +++ b/robosystems_client/models/forecast_month_lite.py @@ -0,0 +1,135 @@ +from __future__ import annotations + +import datetime +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="ForecastMonthLite") + + +@_attrs_define +class ForecastMonthLite: + """One computed forward month in a ``compute-forecast`` response. + + Attributes: + period (str): Month key (``YYYY-MM``). + period_start (datetime.date): + period_end (datetime.date): + income_statement_fact_set_id (None | str | Unset): Scenario IS FactSet upserted for the month. + balance_sheet_fact_set_id (None | str | Unset): Scenario BS FactSet upserted for the month (working-capital + instants only in F-1 — the full BS roll is a later phase). + computed_count (int | Unset): Number of facts emitted for the month across both sets. Default: 0. + """ + + period: str + period_start: datetime.date + period_end: datetime.date + income_statement_fact_set_id: None | str | Unset = UNSET + balance_sheet_fact_set_id: None | str | Unset = UNSET + computed_count: int | Unset = 0 + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + period = self.period + + period_start = self.period_start.isoformat() + + period_end = self.period_end.isoformat() + + income_statement_fact_set_id: None | str | Unset + if isinstance(self.income_statement_fact_set_id, Unset): + income_statement_fact_set_id = UNSET + else: + income_statement_fact_set_id = self.income_statement_fact_set_id + + balance_sheet_fact_set_id: None | str | Unset + if isinstance(self.balance_sheet_fact_set_id, Unset): + balance_sheet_fact_set_id = UNSET + else: + balance_sheet_fact_set_id = self.balance_sheet_fact_set_id + + computed_count = self.computed_count + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "period": period, + "period_start": period_start, + "period_end": period_end, + } + ) + if income_statement_fact_set_id is not UNSET: + field_dict["income_statement_fact_set_id"] = income_statement_fact_set_id + if balance_sheet_fact_set_id is not UNSET: + field_dict["balance_sheet_fact_set_id"] = balance_sheet_fact_set_id + if computed_count is not UNSET: + field_dict["computed_count"] = computed_count + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + period = d.pop("period") + + period_start = datetime.date.fromisoformat(d.pop("period_start")) + + period_end = datetime.date.fromisoformat(d.pop("period_end")) + + def _parse_income_statement_fact_set_id(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + income_statement_fact_set_id = _parse_income_statement_fact_set_id( + d.pop("income_statement_fact_set_id", UNSET) + ) + + def _parse_balance_sheet_fact_set_id(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + balance_sheet_fact_set_id = _parse_balance_sheet_fact_set_id( + d.pop("balance_sheet_fact_set_id", UNSET) + ) + + computed_count = d.pop("computed_count", UNSET) + + forecast_month_lite = cls( + period=period, + period_start=period_start, + period_end=period_end, + income_statement_fact_set_id=income_statement_fact_set_id, + balance_sheet_fact_set_id=balance_sheet_fact_set_id, + computed_count=computed_count, + ) + + forecast_month_lite.additional_properties = d + return forecast_month_lite + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/robosystems_client/models/lever_assertion_lite.py b/robosystems_client/models/lever_assertion_lite.py new file mode 100644 index 0000000..f0a271d --- /dev/null +++ b/robosystems_client/models/lever_assertion_lite.py @@ -0,0 +1,120 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.lever_assertion_lite_values_by_period import ( + LeverAssertionLiteValuesByPeriod, + ) + + +T = TypeVar("T", bound="LeverAssertionLite") + + +@_attrs_define +class LeverAssertionLite: + """One lever's persisted assertion inside ``ForecastMechanics``. + + The create handler expands the wire-level assertion (uniform ``value`` + + per-month overrides) into the explicit ``values_by_period`` map so + compute never interpolates — every asserted month is stated. The + values are duplicated as authored facts in the scenario's lever + FactSet (rules for mechanics, **facts for values** — the facts are + what ``compute-forecast`` binds); this mechanics copy is the + operator-legible round-trip shape. + + Attributes: + qname (str): rs-driver lever element qname. + element_id (str): Resolved tenant element id. + values_by_period (LeverAssertionLiteValuesByPeriod): Expanded per-month assertions keyed by ``YYYY-MM``. + item_type (None | str | Unset): Format family from the catalog (percent | days | ...). + """ + + qname: str + element_id: str + values_by_period: LeverAssertionLiteValuesByPeriod + item_type: None | str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + qname = self.qname + + element_id = self.element_id + + values_by_period = self.values_by_period.to_dict() + + item_type: None | str | Unset + if isinstance(self.item_type, Unset): + item_type = UNSET + else: + item_type = self.item_type + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "qname": qname, + "element_id": element_id, + "values_by_period": values_by_period, + } + ) + if item_type is not UNSET: + field_dict["item_type"] = item_type + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.lever_assertion_lite_values_by_period import ( + LeverAssertionLiteValuesByPeriod, + ) + + d = dict(src_dict) + qname = d.pop("qname") + + element_id = d.pop("element_id") + + values_by_period = LeverAssertionLiteValuesByPeriod.from_dict( + d.pop("values_by_period") + ) + + def _parse_item_type(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + item_type = _parse_item_type(d.pop("item_type", UNSET)) + + lever_assertion_lite = cls( + qname=qname, + element_id=element_id, + values_by_period=values_by_period, + item_type=item_type, + ) + + lever_assertion_lite.additional_properties = d + return lever_assertion_lite + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/robosystems_client/models/lever_assertion_lite_values_by_period.py b/robosystems_client/models/lever_assertion_lite_values_by_period.py new file mode 100644 index 0000000..eea7030 --- /dev/null +++ b/robosystems_client/models/lever_assertion_lite_values_by_period.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="LeverAssertionLiteValuesByPeriod") + + +@_attrs_define +class LeverAssertionLiteValuesByPeriod: + """Expanded per-month assertions keyed by ``YYYY-MM``.""" + + additional_properties: dict[str, float] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + lever_assertion_lite_values_by_period = cls() + + lever_assertion_lite_values_by_period.additional_properties = d + return lever_assertion_lite_values_by_period + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> float: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: float) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/robosystems_client/models/lever_assertion_request.py b/robosystems_client/models/lever_assertion_request.py new file mode 100644 index 0000000..6d5adb4 --- /dev/null +++ b/robosystems_client/models/lever_assertion_request.py @@ -0,0 +1,144 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.lever_assertion_request_values_by_period_type_0 import ( + LeverAssertionRequestValuesByPeriodType0, + ) + + +T = TypeVar("T", bound="LeverAssertionRequest") + + +@_attrs_define +class LeverAssertionRequest: + """One lever's asserted values for the scenario. + + ``qname`` must resolve to an ``rs-driver:*`` catalog element (the + create handler rejects anything else). Value conventions follow the + catalog: percent levers are decimals per month (0.03 = 3%/month), + days levers are day counts. + + ``value`` is a uniform fill across the whole horizon; + ``values_by_period`` overrides individual months (``"YYYY-MM"`` + keys). At least one of the two must be provided. Months covered by + neither carry no assertion — the lever's rule is inactive for that + month and its target falls to the engine's carry-forward default. + + Attributes: + qname (str): QName of the rs-driver lever element (e.g. ``rs-driver:RevenueGrowthRate``). + value (float | None | Unset): Uniform value asserted for every month of the horizon. + values_by_period (LeverAssertionRequestValuesByPeriodType0 | None | Unset): Per-month overrides keyed by ``YYYY- + MM``. Wins over ``value`` for the months it names. + """ + + qname: str + value: float | None | Unset = UNSET + values_by_period: LeverAssertionRequestValuesByPeriodType0 | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.lever_assertion_request_values_by_period_type_0 import ( + LeverAssertionRequestValuesByPeriodType0, + ) + + qname = self.qname + + value: float | None | Unset + if isinstance(self.value, Unset): + value = UNSET + else: + value = self.value + + values_by_period: dict[str, Any] | None | Unset + if isinstance(self.values_by_period, Unset): + values_by_period = UNSET + elif isinstance(self.values_by_period, LeverAssertionRequestValuesByPeriodType0): + values_by_period = self.values_by_period.to_dict() + else: + values_by_period = self.values_by_period + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "qname": qname, + } + ) + if value is not UNSET: + field_dict["value"] = value + if values_by_period is not UNSET: + field_dict["values_by_period"] = values_by_period + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.lever_assertion_request_values_by_period_type_0 import ( + LeverAssertionRequestValuesByPeriodType0, + ) + + d = dict(src_dict) + qname = d.pop("qname") + + def _parse_value(data: object) -> float | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(float | None | Unset, data) + + value = _parse_value(d.pop("value", UNSET)) + + def _parse_values_by_period( + data: object, + ) -> LeverAssertionRequestValuesByPeriodType0 | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + values_by_period_type_0 = LeverAssertionRequestValuesByPeriodType0.from_dict( + data + ) + + return values_by_period_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(LeverAssertionRequestValuesByPeriodType0 | None | Unset, data) + + values_by_period = _parse_values_by_period(d.pop("values_by_period", UNSET)) + + lever_assertion_request = cls( + qname=qname, + value=value, + values_by_period=values_by_period, + ) + + lever_assertion_request.additional_properties = d + return lever_assertion_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/robosystems_client/models/lever_assertion_request_values_by_period_type_0.py b/robosystems_client/models/lever_assertion_request_values_by_period_type_0.py new file mode 100644 index 0000000..775913c --- /dev/null +++ b/robosystems_client/models/lever_assertion_request_values_by_period_type_0.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="LeverAssertionRequestValuesByPeriodType0") + + +@_attrs_define +class LeverAssertionRequestValuesByPeriodType0: + """ """ + + additional_properties: dict[str, float] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + lever_assertion_request_values_by_period_type_0 = cls() + + lever_assertion_request_values_by_period_type_0.additional_properties = d + return lever_assertion_request_values_by_period_type_0 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> float: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: float) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/robosystems_client/models/operation_envelope_compute_forecast_response.py b/robosystems_client/models/operation_envelope_compute_forecast_response.py new file mode 100644 index 0000000..d3bc2df --- /dev/null +++ b/robosystems_client/models/operation_envelope_compute_forecast_response.py @@ -0,0 +1,158 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.operation_envelope_compute_forecast_response_status import ( + OperationEnvelopeComputeForecastResponseStatus, +) +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.compute_forecast_response import ComputeForecastResponse + + +T = TypeVar("T", bound="OperationEnvelopeComputeForecastResponse") + + +@_attrs_define +class OperationEnvelopeComputeForecastResponse: + """ + Attributes: + operation (str): Kebab-case operation name + operation_id (str): op_-prefixed ULID for audit and SSE correlation + status (OperationEnvelopeComputeForecastResponseStatus): Operation lifecycle state + at (str): ISO-8601 UTC timestamp + result (ComputeForecastResponse | None | Unset): Command-specific result payload + created_by (None | str | Unset): User ID that initiated the operation (null for legacy callers) + idempotent_replay (bool | Unset): True when this envelope came from the idempotency cache — the underlying + command did not execute again. False on fresh executions. Default: False. + """ + + operation: str + operation_id: str + status: OperationEnvelopeComputeForecastResponseStatus + at: str + result: ComputeForecastResponse | None | Unset = UNSET + created_by: None | str | Unset = UNSET + idempotent_replay: bool | Unset = False + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.compute_forecast_response import ComputeForecastResponse + + operation = self.operation + + operation_id = self.operation_id + + status = self.status.value + + at = self.at + + result: dict[str, Any] | None | Unset + if isinstance(self.result, Unset): + result = UNSET + elif isinstance(self.result, ComputeForecastResponse): + result = self.result.to_dict() + else: + result = self.result + + created_by: None | str | Unset + if isinstance(self.created_by, Unset): + created_by = UNSET + else: + created_by = self.created_by + + idempotent_replay = self.idempotent_replay + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "operation": operation, + "operationId": operation_id, + "status": status, + "at": at, + } + ) + if result is not UNSET: + field_dict["result"] = result + if created_by is not UNSET: + field_dict["createdBy"] = created_by + if idempotent_replay is not UNSET: + field_dict["idempotentReplay"] = idempotent_replay + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.compute_forecast_response import ComputeForecastResponse + + d = dict(src_dict) + operation = d.pop("operation") + + operation_id = d.pop("operationId") + + status = OperationEnvelopeComputeForecastResponseStatus(d.pop("status")) + + at = d.pop("at") + + def _parse_result(data: object) -> ComputeForecastResponse | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + result_type_0 = ComputeForecastResponse.from_dict(data) + + return result_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(ComputeForecastResponse | None | Unset, data) + + result = _parse_result(d.pop("result", UNSET)) + + def _parse_created_by(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + created_by = _parse_created_by(d.pop("createdBy", UNSET)) + + idempotent_replay = d.pop("idempotentReplay", UNSET) + + operation_envelope_compute_forecast_response = cls( + operation=operation, + operation_id=operation_id, + status=status, + at=at, + result=result, + created_by=created_by, + idempotent_replay=idempotent_replay, + ) + + operation_envelope_compute_forecast_response.additional_properties = d + return operation_envelope_compute_forecast_response + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/robosystems_client/models/operation_envelope_compute_forecast_response_status.py b/robosystems_client/models/operation_envelope_compute_forecast_response_status.py new file mode 100644 index 0000000..1f90b39 --- /dev/null +++ b/robosystems_client/models/operation_envelope_compute_forecast_response_status.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class OperationEnvelopeComputeForecastResponseStatus(str, Enum): + COMPLETED = "completed" + FAILED = "failed" + PENDING = "pending" + + def __str__(self) -> str: + return str(self.value) diff --git a/robosystems_client/models/skipped_forecast_lite.py b/robosystems_client/models/skipped_forecast_lite.py new file mode 100644 index 0000000..9e73341 --- /dev/null +++ b/robosystems_client/models/skipped_forecast_lite.py @@ -0,0 +1,126 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="SkippedForecastLite") + + +@_attrs_define +class SkippedForecastLite: + """One rule/month soft-skip in a ``compute-forecast`` response. + + A skipped rule never aborts the walk — its target falls back to the + carry-forward value for that month (when a prior value exists). + + Attributes: + period (str): Month key (``YYYY-MM``) of the skip. + reason (str): + rule_id (None | str | Unset): + element_qname (None | str | Unset): + missing (list[str] | Unset): + """ + + period: str + reason: str + rule_id: None | str | Unset = UNSET + element_qname: None | str | Unset = UNSET + missing: list[str] | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + period = self.period + + reason = self.reason + + rule_id: None | str | Unset + if isinstance(self.rule_id, Unset): + rule_id = UNSET + else: + rule_id = self.rule_id + + element_qname: None | str | Unset + if isinstance(self.element_qname, Unset): + element_qname = UNSET + else: + element_qname = self.element_qname + + missing: list[str] | Unset = UNSET + if not isinstance(self.missing, Unset): + missing = self.missing + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "period": period, + "reason": reason, + } + ) + if rule_id is not UNSET: + field_dict["rule_id"] = rule_id + if element_qname is not UNSET: + field_dict["element_qname"] = element_qname + if missing is not UNSET: + field_dict["missing"] = missing + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + period = d.pop("period") + + reason = d.pop("reason") + + def _parse_rule_id(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + rule_id = _parse_rule_id(d.pop("rule_id", UNSET)) + + def _parse_element_qname(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + element_qname = _parse_element_qname(d.pop("element_qname", UNSET)) + + missing = cast(list[str], d.pop("missing", UNSET)) + + skipped_forecast_lite = cls( + period=period, + reason=reason, + rule_id=rule_id, + element_qname=element_qname, + missing=missing, + ) + + skipped_forecast_lite.additional_properties = d + return skipped_forecast_lite + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/robosystems_client/models/update_forecast_arm.py b/robosystems_client/models/update_forecast_arm.py new file mode 100644 index 0000000..cdc5a8b --- /dev/null +++ b/robosystems_client/models/update_forecast_arm.py @@ -0,0 +1,88 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Literal, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.update_forecast_request import UpdateForecastRequest + + +T = TypeVar("T", bound="UpdateForecastArm") + + +@_attrs_define +class UpdateForecastArm: + """Update-information-block body for ``block_type="forecast"``. + + Mutable: name, scenario_kind, horizon_months, base_period, levers + (full replace). Updating does not recompute — run ``compute-forecast`` + to refresh the scenario's derived months. + + Attributes: + block_type (Literal['forecast']): Discriminator value selecting this arm. + payload (UpdateForecastRequest): Update a forecast block in place. + + Mutable: name, scenario_kind, horizon_months, base_period, levers. + ``levers`` is a **full replace** when provided (partial lever edits + would make the asserted set ambiguous). Updating does NOT recompute — + previously computed scenario months go stale until the next + ``compute-forecast`` run (the compute-metrics drift semantics). + """ + + block_type: Literal["forecast"] + payload: UpdateForecastRequest + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + block_type = self.block_type + + payload = self.payload.to_dict() + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "block_type": block_type, + "payload": payload, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.update_forecast_request import UpdateForecastRequest + + d = dict(src_dict) + block_type = cast(Literal["forecast"], d.pop("block_type")) + if block_type != "forecast": + raise ValueError(f"block_type must match const 'forecast', got '{block_type}'") + + payload = UpdateForecastRequest.from_dict(d.pop("payload")) + + update_forecast_arm = cls( + block_type=block_type, + payload=payload, + ) + + update_forecast_arm.additional_properties = d + return update_forecast_arm + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/robosystems_client/models/update_forecast_request.py b/robosystems_client/models/update_forecast_request.py new file mode 100644 index 0000000..a6e6725 --- /dev/null +++ b/robosystems_client/models/update_forecast_request.py @@ -0,0 +1,210 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.update_forecast_request_scenario_kind_type_0 import ( + UpdateForecastRequestScenarioKindType0, +) +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.lever_assertion_request import LeverAssertionRequest + + +T = TypeVar("T", bound="UpdateForecastRequest") + + +@_attrs_define +class UpdateForecastRequest: + """Update a forecast block in place. + + Mutable: name, scenario_kind, horizon_months, base_period, levers. + ``levers`` is a **full replace** when provided (partial lever edits + would make the asserted set ambiguous). Updating does NOT recompute — + previously computed scenario months go stale until the next + ``compute-forecast`` run (the compute-metrics drift semantics). + + Attributes: + structure_id (str): Structure ID of the forecast block. + name (None | str | Unset): + scenario_kind (None | Unset | UpdateForecastRequestScenarioKindType0): + horizon_months (int | None | Unset): + base_period (None | str | Unset): + levers (list[LeverAssertionRequest] | None | Unset): Full replacement of the lever set when provided. + """ + + structure_id: str + name: None | str | Unset = UNSET + scenario_kind: None | Unset | UpdateForecastRequestScenarioKindType0 = UNSET + horizon_months: int | None | Unset = UNSET + base_period: None | str | Unset = UNSET + levers: list[LeverAssertionRequest] | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + structure_id = self.structure_id + + name: None | str | Unset + if isinstance(self.name, Unset): + name = UNSET + else: + name = self.name + + scenario_kind: None | str | Unset + if isinstance(self.scenario_kind, Unset): + scenario_kind = UNSET + elif isinstance(self.scenario_kind, UpdateForecastRequestScenarioKindType0): + scenario_kind = self.scenario_kind.value + else: + scenario_kind = self.scenario_kind + + horizon_months: int | None | Unset + if isinstance(self.horizon_months, Unset): + horizon_months = UNSET + else: + horizon_months = self.horizon_months + + base_period: None | str | Unset + if isinstance(self.base_period, Unset): + base_period = UNSET + else: + base_period = self.base_period + + levers: list[dict[str, Any]] | None | Unset + if isinstance(self.levers, Unset): + levers = UNSET + elif isinstance(self.levers, list): + levers = [] + for levers_type_0_item_data in self.levers: + levers_type_0_item = levers_type_0_item_data.to_dict() + levers.append(levers_type_0_item) + + else: + levers = self.levers + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "structure_id": structure_id, + } + ) + if name is not UNSET: + field_dict["name"] = name + if scenario_kind is not UNSET: + field_dict["scenario_kind"] = scenario_kind + if horizon_months is not UNSET: + field_dict["horizon_months"] = horizon_months + if base_period is not UNSET: + field_dict["base_period"] = base_period + if levers is not UNSET: + field_dict["levers"] = levers + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.lever_assertion_request import LeverAssertionRequest + + d = dict(src_dict) + structure_id = d.pop("structure_id") + + def _parse_name(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + name = _parse_name(d.pop("name", UNSET)) + + def _parse_scenario_kind( + data: object, + ) -> None | Unset | UpdateForecastRequestScenarioKindType0: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + scenario_kind_type_0 = UpdateForecastRequestScenarioKindType0(data) + + return scenario_kind_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | Unset | UpdateForecastRequestScenarioKindType0, data) + + scenario_kind = _parse_scenario_kind(d.pop("scenario_kind", UNSET)) + + def _parse_horizon_months(data: object) -> int | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(int | None | Unset, data) + + horizon_months = _parse_horizon_months(d.pop("horizon_months", UNSET)) + + def _parse_base_period(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + base_period = _parse_base_period(d.pop("base_period", UNSET)) + + def _parse_levers(data: object) -> list[LeverAssertionRequest] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + levers_type_0 = [] + _levers_type_0 = data + for levers_type_0_item_data in _levers_type_0: + levers_type_0_item = LeverAssertionRequest.from_dict(levers_type_0_item_data) + + levers_type_0.append(levers_type_0_item) + + return levers_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[LeverAssertionRequest] | None | Unset, data) + + levers = _parse_levers(d.pop("levers", UNSET)) + + update_forecast_request = cls( + structure_id=structure_id, + name=name, + scenario_kind=scenario_kind, + horizon_months=horizon_months, + base_period=base_period, + levers=levers, + ) + + update_forecast_request.additional_properties = d + return update_forecast_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/robosystems_client/models/update_forecast_request_scenario_kind_type_0.py b/robosystems_client/models/update_forecast_request_scenario_kind_type_0.py new file mode 100644 index 0000000..8eab2cd --- /dev/null +++ b/robosystems_client/models/update_forecast_request_scenario_kind_type_0.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class UpdateForecastRequestScenarioKindType0(str, Enum): + BUDGET = "budget" + FORECAST = "forecast" + PROJECTION = "projection" + + def __str__(self) -> str: + return str(self.value)