Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions .github/release-notes/v1.12.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
The generated tier catches up with the API's 1.10 line — the OAuth
authorization server, reconciling items on the close, and the removal that
came with them. This release names that removal, and nothing on the stable
tier changes shape.

## Removed: the REST MCP tool surface

`robosystems_client.api.mcp.list_mcp_tools` and `call_mcp_tool`, with the
models `MCPToolCall`, `MCPToolCallArguments`, `MCPToolsResponse` and
`MCPToolsResponseToolsItem`, are gone. The API removed
`GET /v1/graphs/{graph_id}/mcp/tools` and `POST …/mcp/call-tool` in 1.10.2:
MCP clients speak the Streamable HTTP transport directly —
`POST /v1/graphs/{graph_id}/mcp` with the same `X-API-Key`, or the OAuth-only
`POST /v1/mcp` — and thirty days of production traffic showed no caller on the
REST pair. The facades never wrapped them and the integration template's emit
path never imported them, so this is a generated-tier removal riding a minor.
If you did call them, the replacement is a JSON-RPC `tools/list` or
`tools/call` against the same graph URL.

## New: connected apps

`api.user.list_user_o_auth_grants` and `revoke_user_o_auth_grant`
(`GET` / `DELETE /v1/user/oauth/grants`), with `OAuthGrantInfo` and
`OAuthGrantsResponse`. A grant is one OAuth consent — one MCP client on one
graph. The listing shows the client, the graph it reaches, the MCP URL its
tokens are bound to, and when it was last used; revoking one kills every token
minted from it, so the client fails at its next call and has to ask the user
again. Until now the only way to revoke a connection was a password change,
which revoked all of them.

## New: reconciling items

`api.extensions_robo_ledger.preview_reconciling_item` and
`resolve_reconciling_item`
(`POST /extensions/roboledger/{graph_id}/operations/preview-reconciling-item`
and `…/resolve-reconciling-item`), with their request, plan and response
models. A reconciling item is a posted event whose source payload changed
afterwards — a difference between the books and the source system that nobody
has dispositioned. `FiscalCalendarResponse` gains `reconciling_item_count` and
`reconciling_item_sample` (up to five identifiers) so a blocked close names
what is holding it; `ClosePeriodOperation` and `BackfillPlanHistoryOperation`
gain `allow_reconciling_items` to close over them knowingly, which the close
audit note records. The checked-in GraphQL schema gains the matching
`reconcilingItemCount` and `reconcilingItemSample` fields.

## Also

- `DeleteSubgraphOp.backup_first` now says what the backup is: a full dump
registered on the parent graph's backup list, downloadable after the
subgraph is gone — and a failed backup aborts the delete.
- `CreateEventBlock`'s description states how a `journal_entry_recorded`
event decides whether to write back to a connected source system, and when
to set `metadata.publish_to_source` to false.

Nothing is deprecated.
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,11 @@ def sync_detailed(

Persist a real-world business event. apply_handlers=False (default): capture-only,
status='captured'. apply_handlers=True: resolves an event_handler, fires the template, creates GL
entries atomically, status='classified'. Use preview-event-block to dry-run before committing.
entries atomically, status='classified'. Use preview-event-block to dry-run before committing. For
journal_entry_recorded, whether the entry writes back to a connected source system follows `source`
(schedule/manual publish; system does not) unless metadata.publish_to_source says otherwise — set it
false for an alignment entry mirroring a change already made upstream, which would otherwise be
applied twice.

**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.
Expand Down Expand Up @@ -157,7 +161,11 @@ def sync(

Persist a real-world business event. apply_handlers=False (default): capture-only,
status='captured'. apply_handlers=True: resolves an event_handler, fires the template, creates GL
entries atomically, status='classified'. Use preview-event-block to dry-run before committing.
entries atomically, status='classified'. Use preview-event-block to dry-run before committing. For
journal_entry_recorded, whether the entry writes back to a connected source system follows `source`
(schedule/manual publish; system does not) unless metadata.publish_to_source says otherwise — set it
false for an alignment entry mirroring a change already made upstream, which would otherwise be
applied twice.

**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.
Expand Down Expand Up @@ -194,7 +202,11 @@ async def asyncio_detailed(

Persist a real-world business event. apply_handlers=False (default): capture-only,
status='captured'. apply_handlers=True: resolves an event_handler, fires the template, creates GL
entries atomically, status='classified'. Use preview-event-block to dry-run before committing.
entries atomically, status='classified'. Use preview-event-block to dry-run before committing. For
journal_entry_recorded, whether the entry writes back to a connected source system follows `source`
(schedule/manual publish; system does not) unless metadata.publish_to_source says otherwise — set it
false for an alignment entry mirroring a change already made upstream, which would otherwise be
applied twice.

**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.
Expand Down Expand Up @@ -234,7 +246,11 @@ async def asyncio(

Persist a real-world business event. apply_handlers=False (default): capture-only,
status='captured'. apply_handlers=True: resolves an event_handler, fires the template, creates GL
entries atomically, status='classified'. Use preview-event-block to dry-run before committing.
entries atomically, status='classified'. Use preview-event-block to dry-run before committing. For
journal_entry_recorded, whether the entry writes back to a connected source system follows `source`
(schedule/manual publish; system does not) unless metadata.publish_to_source says otherwise — set it
false for an alignment entry mirroring a change already made upstream, which would otherwise be
applied twice.

**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.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
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.error_response import ErrorResponse
from ...models.operation_envelope_reconciling_item_plan import (
OperationEnvelopeReconcilingItemPlan,
)
from ...models.preview_reconciling_item_request import PreviewReconcilingItemRequest
from ...types import UNSET, Response, Unset


def _get_kwargs(
graph_id: str,
*,
body: PreviewReconcilingItemRequest,
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/preview-reconciling-item".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 | OperationEnvelopeReconcilingItemPlan | None:
if response.status_code == 200:
response_200 = OperationEnvelopeReconcilingItemPlan.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 | OperationEnvelopeReconcilingItemPlan]:
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: PreviewReconcilingItemRequest,
idempotency_key: None | str | Unset = UNSET,
) -> Response[ErrorResponse | OperationEnvelopeReconcilingItemPlan]:
"""Preview Reconciling Item

Read what changed on a reconciling item — an event whose source-system payload changed after it was
posted (list them with list-event-blocks is_reconciling_item=true). Returns the posted entries
against the accepted payload, the per-account net difference, which disposition applies by default,
and anything blocking the others. Writes nothing. Run this before resolve-reconciling-item and agree
the treatment with the user — restate moves prior months' figures, catch_up does not.

**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 (PreviewReconcilingItemRequest): Read what changed on a reconciling item, and what
resolving it would do.

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 | OperationEnvelopeReconcilingItemPlan]
"""

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: PreviewReconcilingItemRequest,
idempotency_key: None | str | Unset = UNSET,
) -> ErrorResponse | OperationEnvelopeReconcilingItemPlan | None:
"""Preview Reconciling Item

Read what changed on a reconciling item — an event whose source-system payload changed after it was
posted (list them with list-event-blocks is_reconciling_item=true). Returns the posted entries
against the accepted payload, the per-account net difference, which disposition applies by default,
and anything blocking the others. Writes nothing. Run this before resolve-reconciling-item and agree
the treatment with the user — restate moves prior months' figures, catch_up does not.

**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 (PreviewReconcilingItemRequest): Read what changed on a reconciling item, and what
resolving it would do.

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 | OperationEnvelopeReconcilingItemPlan
"""

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: PreviewReconcilingItemRequest,
idempotency_key: None | str | Unset = UNSET,
) -> Response[ErrorResponse | OperationEnvelopeReconcilingItemPlan]:
"""Preview Reconciling Item

Read what changed on a reconciling item — an event whose source-system payload changed after it was
posted (list them with list-event-blocks is_reconciling_item=true). Returns the posted entries
against the accepted payload, the per-account net difference, which disposition applies by default,
and anything blocking the others. Writes nothing. Run this before resolve-reconciling-item and agree
the treatment with the user — restate moves prior months' figures, catch_up does not.

**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 (PreviewReconcilingItemRequest): Read what changed on a reconciling item, and what
resolving it would do.

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 | OperationEnvelopeReconcilingItemPlan]
"""

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: PreviewReconcilingItemRequest,
idempotency_key: None | str | Unset = UNSET,
) -> ErrorResponse | OperationEnvelopeReconcilingItemPlan | None:
"""Preview Reconciling Item

Read what changed on a reconciling item — an event whose source-system payload changed after it was
posted (list them with list-event-blocks is_reconciling_item=true). Returns the posted entries
against the accepted payload, the per-account net difference, which disposition applies by default,
and anything blocking the others. Writes nothing. Run this before resolve-reconciling-item and agree
the treatment with the user — restate moves prior months' figures, catch_up does not.

**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 (PreviewReconcilingItemRequest): Read what changed on a reconciling item, and what
resolving it would do.

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 | OperationEnvelopeReconcilingItemPlan
"""

return (
await asyncio_detailed(
graph_id=graph_id,
client=client,
body=body,
idempotency_key=idempotency_key,
)
).parsed
Loading