From a41a447182d92d69dcd3ad6e7ab0ec02f1af58bc Mon Sep 17 00:00:00 2001 From: Kolawole Mangabo Date: Tue, 11 Aug 2026 16:59:36 +0100 Subject: [PATCH] feat: sync SDK with public OpenAPI contract --- .bumpversion.cfg | 2 +- .conductor/settings.toml | 18 + .github/workflows/ci.yml | 1 + .github/workflows/sync-contract.yml | 36 + .gitignore | 1 + .openapi-contract.json | 4 + AGENTS.md | 29 + CHANGELOG.md | 8 + README.md | 129 +-- UPGRADING.md | 17 + pyproject.toml | 2 +- scripts/fetch_contract.py | 65 ++ src/reconify/__init__.py | 2 +- src/reconify/client.py | 139 +-- src/reconify/models.py | 1130 +++------------------ src/reconify/pagination.py | 2 +- src/reconify/resources/__init__.py | 114 +-- src/reconify/resources/alerts.py | 58 -- src/reconify/resources/events.py | 59 +- src/reconify/resources/ingestion.py | 78 +- src/reconify/resources/issues.py | 194 ++-- src/reconify/resources/ledger.py | 205 ---- src/reconify/resources/metadata.py | 24 + src/reconify/resources/organization.py | 41 + src/reconify/resources/reconciliations.py | 236 ----- src/reconify/resources/search.py | 37 - src/reconify/resources/setup.py | 311 ------ src/reconify/resources/transactions.py | 58 -- src/reconify/resources/wallets.py | 79 -- src/reconify/transport.py | 10 +- tests/test_client.py | 220 ++-- tests/test_openapi_coverage.py | 78 +- tests/test_pagination.py | 29 +- uv.lock | 2 +- 34 files changed, 733 insertions(+), 2685 deletions(-) create mode 100644 .conductor/settings.toml create mode 100644 .github/workflows/sync-contract.yml create mode 100644 .openapi-contract.json create mode 100644 AGENTS.md create mode 100644 UPGRADING.md create mode 100644 scripts/fetch_contract.py delete mode 100644 src/reconify/resources/alerts.py delete mode 100644 src/reconify/resources/ledger.py create mode 100644 src/reconify/resources/metadata.py create mode 100644 src/reconify/resources/organization.py delete mode 100644 src/reconify/resources/reconciliations.py delete mode 100644 src/reconify/resources/search.py delete mode 100644 src/reconify/resources/setup.py delete mode 100644 src/reconify/resources/transactions.py delete mode 100644 src/reconify/resources/wallets.py diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 6bbc4c0..68a2f36 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.1.0 +current_version = 1.0.0 commit = True tag = True tag_name = v{new_version} diff --git a/.conductor/settings.toml b/.conductor/settings.toml new file mode 100644 index 0000000..0a014a4 --- /dev/null +++ b/.conductor/settings.toml @@ -0,0 +1,18 @@ +"$schema" = "https://conductor.build/schemas/settings.repo.schema.json" + +[scripts] +setup = "python3 -m venv .venv && .venv/bin/pip install -e '.[dev]'" +run_mode = "concurrent" + +[scripts.run.verify] +command = ".venv/bin/python scripts/fetch_contract.py && .venv/bin/ruff check . && .venv/bin/mypy src && .venv/bin/pytest -q && .venv/bin/python -m build" +icon = "check" +default = true + +[scripts.run.sync-contract] +command = ".venv/bin/python scripts/fetch_contract.py --latest && .venv/bin/python -m pytest -q tests/test_openapi_coverage.py" +icon = "refresh-cw" + +[scripts.run.release-check] +command = ".venv/bin/ruff check . && .venv/bin/mypy src && .venv/bin/pytest -q && .venv/bin/python -m build" +icon = "package-check" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d678f66..42ad49b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,7 @@ jobs: python-version: "3.12" - run: python -m pip install --upgrade pip - run: pip install ".[dev]" + - run: python scripts/fetch_contract.py - run: ruff check . - run: mypy src - run: pytest -q diff --git a/.github/workflows/sync-contract.yml b/.github/workflows/sync-contract.yml new file mode 100644 index 0000000..93c04d2 --- /dev/null +++ b/.github/workflows/sync-contract.yml @@ -0,0 +1,36 @@ +name: Sync public OpenAPI contract + +on: + workflow_dispatch: + schedule: + - cron: "23 6 * * 1" + +permissions: + contents: write + pull-requests: write + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: python -m pip install --upgrade pip + - run: pip install ".[dev]" + - run: python scripts/fetch_contract.py --latest + - run: python -m pytest -q tests/test_openapi_coverage.py + - run: ruff check . + - run: mypy src + - run: pytest -q + - run: python -m build + - uses: peter-evans/create-pull-request@v7 + with: + branch: automation/openapi-contract-sync + delete-branch: true + commit-message: "chore: sync public OpenAPI contract" + title: "chore: sync public OpenAPI contract" + body: | + This automated update was generated from the public Reconify OpenAPI + manifest. Review generated API models, examples, and migration notes before merging. diff --git a/.gitignore b/.gitignore index f02b17f..e24593b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ __pycache__/ .mypy_cache/ .ruff_cache/ .venv/ +.contract/ dist/ build/ *.egg-info/ diff --git a/.openapi-contract.json b/.openapi-contract.json new file mode 100644 index 0000000..c59262e --- /dev/null +++ b/.openapi-contract.json @@ -0,0 +1,4 @@ +{ + "version": "1.0.0", + "sha256": "04170b7eda8a7bacef6591a7132b9a5cbdb62963af9ca6260e1948c1ee8233c1" +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..551d354 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,29 @@ +# Reconify Python SDK agent guide + +This repository publishes reconify-python, the typed synchronous and +asynchronous client for the public Reconify /v1 API. + +## Contract authority + +The SaaS Go API owns the contract. Fetch the pinned public artifact with +python scripts/fetch_contract.py, or update to the manifest version with +python scripts/fetch_contract.py --latest. + +For local SaaS work, set RECONIFY_OPENAPI_SPEC to an explicit OpenAPI JSON +file. Do not use sibling repositories or absolute workspace paths. + +The resource and transport layers are handwritten for Python ergonomics. +tests/test_openapi_coverage.py is the contract boundary and must remain +spec-driven. Keep Pydantic models aligned with the downloaded schemas. + +## Supported surface + +The SDK exposes metadata, events, ingestion, issues, and organization +resources. Internal /business/v1 routes and the former ledger, wallet, setup, +search, alert, and reconciliation surface are not public SDK APIs. + +## Verification + +Run ruff check ., mypy src, pytest -q, and python -m build before releasing. +Keep credentials out of errors and logs, preserve request IDs, and update +examples and migration notes when the public contract changes. diff --git a/CHANGELOG.md b/CHANGELOG.md index 7329c3e..e16bec8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 1.0.0 + +- Rebuilt the client for the current 13-operation public monitoring API. +- Added metadata, organization, issue notes, and issue-linked event resources. +- Removed the obsolete ledger, wallet, setup, search, alert, and reconciliation + surface. +- Added pinned public OpenAPI synchronization and contract coverage checks. + ## 0.1.0 - Initial typed Reconify Python SDK. diff --git a/README.md b/README.md index 4381176..1f16bcd 100644 --- a/README.md +++ b/README.md @@ -1,107 +1,62 @@ # Reconify Python SDK -Typed synchronous and asynchronous clients for the Reconify Public API. +Typed synchronous and asynchronous clients for the public Reconify v1 API. -## Installation +## Installation and quickstart -```bash -pip install reconify-python -``` + pip install reconify-python -## Quickstart + from reconify import Reconify -```python -from reconify import Reconify + with Reconify(api_key="rk_...") as client: + events = client.events.list_events(limit=25) + for event in events.events: + print(event.id, event.status) -with Reconify(api_key="rk_...") as client: - sources = client.ledger.list_ledger_sources(limit=25) - for source in sources.sources or []: - print(source.id, source.name) -``` +The API key may also come from RECONIFY_API_KEY. The default endpoint is +https://api.reconifyhq.com/v1. RECONIFY_API_URL or base_url can select a +staging or self-hosted endpoint. The client accepts URLs with or without /v1. -The key may also be supplied through `RECONIFY_API_KEY`. The default endpoint is -`https://api.reconifyhq.com/v1`; pass `base_url="https://staging.example/v1"` -for staging or self-hosted deployments. `/v1` is added when it is absent. +## Public resources -## Async usage and pagination +The client exposes metadata, events, ingestion, issues, and organization. +The current public contract contains 13 operations. Python methods use +snake_case names and Pydantic v2 models from reconify.models. -```python -from reconify import AsyncReconify +Sync and async clients provide cursor iterators: -async with AsyncReconify() as client: - async for event in client.iter_events(limit=100): - print(event.id) -``` + async with AsyncReconify() as client: + async for event in client.iter_events(limit=100): + print(event.id) -Cursor and offset iterators preserve opaque cursors and the server's page size. -When an endpoint accepts both cursor and offset pagination, `after` takes -precedence. +Every operation supports raw=True for RawResponse, and per-request timeout +through the timeout query keyword. API errors expose status_code, detail, code, +validation details, response headers, and request_id without including keys or +request bodies. Safe methods retry bounded 429, 503, and transport failures by +default. Unsafe retries require RetryConfig(retry_unsafe_methods=True). -Every list operation also has a natural iterator on the client, for example -`client.iter_reconciliations(limit=100)` or -`client.iter_wallet_transactions(after="cursor")`. The async equivalent is -an async iterator. Iterators forward query parameters using keyword arguments, -so they never expose transport details. +## Contract synchronization -## Errors and retries + python scripts/fetch_contract.py + python scripts/fetch_contract.py --latest + pytest -q tests/test_openapi_coverage.py -HTTP failures raise typed `ReconifyError` subclasses. Every HTTP error exposes -`status_code`, `detail`, `code`, validation details, response headers, and the -response `request_id` without including credentials or request bodies. +The default source is the public manifest at +https://docs.reconifyhq.com/openapi/manifest.json. For local SaaS changes, +set RECONIFY_OPENAPI_SPEC to an explicit OpenAPI JSON file. The SDK never +depends on another checkout or an absolute workspace path. -429, 503, and transient HTTP transport failures such as timeouts are retried -for safe methods with bounded exponential backoff and jitter. Mutating methods -are not retried unless `RetryConfig(retry_unsafe_methods=True)` is supplied. -Transaction ingestion retries must reuse each row's `idempotencyKey`. +## Migration to 1.0.0 -The client default timeout is 30 seconds. Individual operations can override -it with `timeout=...`, including an `httpx.Timeout` object. Async operations -also support normal `asyncio` cancellation, which is the Python equivalent of -context cancellation in other SDKs. +Version 1.0.0 targets the current monitoring and issue-investigation API. The +former ledger, wallet, setup, search, alert, and reconciliation methods are +removed because they are not part of the public contract. See UPGRADING.md. -Use `raw=True` on any operation to receive status, headers, request ID, and raw -body through `RawResponse`. +## Build and release -## Test-session and ingestion headers + ruff check . + mypy src + pytest -q + python -m build -Integrity ingestion and test-session submission accept -`integrity_test_session=...`, which is sent as `X-Integrity-Test-Session`. -Integrity batches support 1–500 events and ledger transaction batches support -1–5000 transactions; the SDK does not truncate caller input. - -The SDK intentionally excludes reconciliation adjustment, evidence, lifecycle, -report-item, and signoff operations. The retained reconciliation surface is -integrity sources, reconciliation list/create/get, and all schedule operations. - -## API reference - -The public operation methods are grouped by API module. Request bodies use the -typed Pydantic models exported from `reconify.models`; list query parameters use -the OpenAPI names in snake_case. Every operation accepts `raw=True` and a -per-request `timeout` override. - -| Module | Methods | -| --- | --- | -| Alerts | `list_alert_rules`, `put_alert_rule` | -| Events | `list_events`, `get_event`, `reveal_event_field` | -| Ingestion | `ingest_integrity_events`, `ingest_integrity_test_events` | -| Issues | `list_issues`, `get_issue_summary`, `get_issue`, `update_issue`, `list_issue_deliveries`, `retry_issue_delivery`, `add_issue_note`, `resolve_issue` | -| Ledger | `list_ledger_sources`, `create_ledger_source`, `delete_ledger_source`, `get_ledger_source`, `update_ledger_source`, `list_source_periods`, `list_transactions`, `ingest_transactions` | -| Reconciliations | `list_integrity_sources_for_reconciliation`, `list_reconciliation_schedules`, `create_reconciliation_schedule`, `delete_reconciliation_schedule`, `get_reconciliation_schedule`, `update_reconciliation_schedule`, `list_reconciliations`, `create_reconciliation`, `get_reconciliation` | -| Search | `search_integrity_resources` | -| Setup | `list_setup_integrations`, `get_setup_integration`, `list_setup_sources`, `create_setup_source`, `get_setup_source`, `update_setup_source`, `disable_setup_source`, `create_test_session`, `get_test_session`, `get_test_session_result`, `retry_test_session`, `submit_test_session_events` | -| Transactions | `list_wallet_transactions`, `get_wallet_transaction` | -| Wallets | `list_wallets`, `get_wallet`, `get_wallet_balance` | - -## Build and deploy - -Build the distributable artifacts locally or in CI: - -```bash -python -m pip install build -python -m build -``` - -The resulting wheel and source archive in `dist/` are ready for publication to -an internal or public Python package registry. CI builds both artifacts after -running lint, type checking, and tests. +The release workflow publishes the built wheel to PyPI after a GitHub release. diff --git a/UPGRADING.md b/UPGRADING.md new file mode 100644 index 0000000..26c2339 --- /dev/null +++ b/UPGRADING.md @@ -0,0 +1,17 @@ +# Upgrading to 1.0.0 + +The 1.0.0 client is rebuilt against the current public Reconify v1 contract. +It removes methods that described private or retired ledger, wallet, setup, +search, alert, and reconciliation routes. + +Use these resources: + +- metadata: API information and health +- events: event listing, lookup, and issue-linked evidence +- ingestion: monitoring event batches +- issues: issue listing, lookup, assignment, notes, and linked data +- organization: organization and member reads + +Python models use snake_case fields and preserve unknown enum values through +tolerant string enums. Regenerate or refresh the models after downloading a +new public contract version. diff --git a/pyproject.toml b/pyproject.toml index 5c9a26f..0c0e2a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "reconify-python" -version = "0.1.0" +version = "1.0.0" description = "Typed Python client for the Reconify Public API" readme = "README.md" requires-python = ">=3.10" diff --git a/scripts/fetch_contract.py b/scripts/fetch_contract.py new file mode 100644 index 0000000..540e039 --- /dev/null +++ b/scripts/fetch_contract.py @@ -0,0 +1,65 @@ +"""Fetch and verify the pinned public OpenAPI contract.""" + +from __future__ import annotations + +import hashlib +import json +import os +import sys +from pathlib import Path +from urllib.parse import urljoin +from urllib.request import urlopen + +ROOT = Path(__file__).resolve().parents[1] +PIN_PATH = ROOT / ".openapi-contract.json" +DEFAULT_OUTPUT = ROOT / ".contract" / "reconify.openapi.json" +DEFAULT_MANIFEST_URL = "https://docs.reconifyhq.com/openapi/manifest.json" + + +def main() -> None: + output = Path(os.environ.get("RECONIFY_OPENAPI_SPEC", str(DEFAULT_OUTPUT))) + update_to_latest = "--latest" in sys.argv + if os.environ.get("RECONIFY_OPENAPI_SPEC"): + content = output.read_bytes() + if update_to_latest: + document = json.loads(content) + version = document.get("info", {}).get("version") + if not version: + raise SystemExit("Local OpenAPI contract has no info.version") + PIN_PATH.write_text( + json.dumps( + { + "version": version, + "sha256": hashlib.sha256(content).hexdigest(), + }, + indent=2, + ) + + "\n" + ) + print(f"Using local OpenAPI contract: {output}") + return + + pinned = json.loads(PIN_PATH.read_text()) + manifest_url = os.environ.get("RECONIFY_OPENAPI_MANIFEST_URL", DEFAULT_MANIFEST_URL) + with urlopen(manifest_url, timeout=30) as response: + manifest = json.loads(response.read()) + version = manifest["contract_version"] if update_to_latest else pinned["version"] + artifact = manifest["versions"].get(version) + if artifact is None: + raise SystemExit(f"OpenAPI manifest has no version {version}") + if not update_to_latest and artifact["sha256"] != pinned["sha256"]: + raise SystemExit(f"OpenAPI manifest checksum changed for pinned version {version}") + with urlopen(urljoin(manifest_url, artifact["url"]), timeout=30) as response: + content = response.read() + digest = hashlib.sha256(content).hexdigest() + if update_to_latest: + PIN_PATH.write_text(json.dumps({"version": version, "sha256": digest}, indent=2) + "\n") + elif digest != pinned["sha256"]: + raise SystemExit(f"OpenAPI checksum mismatch: expected {pinned['sha256']}, got {digest}") + output.parent.mkdir(parents=True, exist_ok=True) + output.write_bytes(content) + print(f"Fetched OpenAPI {version} to {output}") + + +if __name__ == "__main__": + main() diff --git a/src/reconify/__init__.py b/src/reconify/__init__.py index 76f5f94..465f22f 100644 --- a/src/reconify/__init__.py +++ b/src/reconify/__init__.py @@ -1,6 +1,6 @@ """Typed clients for the Reconify Public API.""" -__version__ = "0.1.0" +__version__ = "1.0.0" from .client import AsyncReconify, Reconify from .errors import ( diff --git a/src/reconify/client.py b/src/reconify/client.py index fadaaa3..4683c86 100644 --- a/src/reconify/client.py +++ b/src/reconify/client.py @@ -9,35 +9,20 @@ import httpx from .errors import ReconifyValidationError -from .pagination import ( - aiter_cursor_pages, - aiter_offset_pages, - iter_cursor_pages, - iter_offset_pages, -) +from .pagination import aiter_cursor_pages, iter_cursor_pages from .resources import ( ASYNC_RESOURCE_CLASSES, SYNC_RESOURCE_CLASSES, - Alerts, - AsyncAlerts, AsyncEvents, AsyncIngestion, AsyncIssues, - AsyncLedger, - AsyncReconciliations, - AsyncSearch, - AsyncSetup, - AsyncTransactions, - AsyncWallets, + AsyncMetadata, + AsyncOrganization, Events, Ingestion, Issues, - Ledger, - Reconciliations, - Search, - Setup, - Transactions, - Wallets, + Metadata, + Organization, ) from .transport import AsyncTransport, RetryConfig, SyncTransport @@ -52,9 +37,7 @@ def _normalize_base_url(base_url: str | None) -> str: def _api_key(api_key: str | None) -> str: value = api_key or os.getenv("RECONIFY_API_KEY") if not value: - raise ReconifyValidationError("An API key is required") - if value.startswith("sk_live_"): - raise ReconifyValidationError("Legacy sk_live_ keys are not supported; use an rk_ key") + raise ReconifyValidationError("An API key is required; set api_key or RECONIFY_API_KEY") if not value.startswith("rk_"): raise ReconifyValidationError("Reconify public API keys must start with rk_") return value @@ -63,16 +46,11 @@ def _api_key(api_key: str | None) -> str: class Reconify: """Synchronous typed Reconify API client.""" - alerts: Alerts + metadata: Metadata events: Events ingestion: Ingestion issues: Issues - ledger: Ledger - reconciliations: Reconciliations - search: Search - setup: Setup - transactions: Transactions - wallets: Wallets + organization: Organization def __init__( self, @@ -114,67 +92,22 @@ def iter_issues(self, **query: Any) -> Iterator[Any]: lambda params: self.issues.list_issues(**params), item_field="issues", query=query ) - def iter_ledger_sources(self, **query: Any) -> Iterator[Any]: - return iter_offset_pages( - lambda params: self.ledger.list_ledger_sources(**params), - item_field="sources", - query=query, - ) - - def iter_ledger_transactions(self, source_id: str, **query: Any) -> Iterator[Any]: - return iter_offset_pages( - lambda params: self.ledger.list_transactions(source_id, **params), - item_field="transactions", - query=query, - ) - - def iter_reconciliation_schedules(self, **query: Any) -> Iterator[Any]: - return iter_offset_pages( - lambda params: self.reconciliations.list_reconciliation_schedules(**params), - item_field="schedules", - query=query, - ) - - def iter_reconciliations(self, **query: Any) -> Iterator[Any]: - return iter_offset_pages( - lambda params: self.reconciliations.list_reconciliations(**params), - item_field="reconciliations", - query=query, - ) - - def iter_setup_sources(self, **query: Any) -> Iterator[Any]: - return iter_offset_pages( - lambda params: self.setup.list_setup_sources(**params), - item_field="sources", - query=query, - ) - - def iter_wallet_transactions(self, **query: Any) -> Iterator[Any]: + def iter_issue_events(self, issue_id: str, **query: Any) -> Iterator[Any]: return iter_cursor_pages( - lambda params: self.transactions.list_wallet_transactions(**params), - item_field="transactions", + lambda params: self.events.list_issue_events(issue_id, **params), + item_field="events", query=query, ) - def iter_wallets(self, **query: Any) -> Iterator[Any]: - return iter_offset_pages( - lambda params: self.wallets.list_wallets(**params), item_field="wallets", query=query - ) - class AsyncReconify: """Asynchronous typed Reconify API client.""" - alerts: AsyncAlerts + metadata: AsyncMetadata events: AsyncEvents ingestion: AsyncIngestion issues: AsyncIssues - ledger: AsyncLedger - reconciliations: AsyncReconciliations - search: AsyncSearch - setup: AsyncSetup - transactions: AsyncTransactions - wallets: AsyncWallets + organization: AsyncOrganization def __init__( self, @@ -216,49 +149,9 @@ def iter_issues(self, **query: Any) -> AsyncIterator[Any]: lambda params: self.issues.list_issues(**params), item_field="issues", query=query ) - def iter_ledger_sources(self, **query: Any) -> AsyncIterator[Any]: - return aiter_offset_pages( - lambda params: self.ledger.list_ledger_sources(**params), - item_field="sources", - query=query, - ) - - def iter_ledger_transactions(self, source_id: str, **query: Any) -> AsyncIterator[Any]: - return aiter_offset_pages( - lambda params: self.ledger.list_transactions(source_id, **params), - item_field="transactions", - query=query, - ) - - def iter_reconciliation_schedules(self, **query: Any) -> AsyncIterator[Any]: - return aiter_offset_pages( - lambda params: self.reconciliations.list_reconciliation_schedules(**params), - item_field="schedules", - query=query, - ) - - def iter_reconciliations(self, **query: Any) -> AsyncIterator[Any]: - return aiter_offset_pages( - lambda params: self.reconciliations.list_reconciliations(**params), - item_field="reconciliations", - query=query, - ) - - def iter_setup_sources(self, **query: Any) -> AsyncIterator[Any]: - return aiter_offset_pages( - lambda params: self.setup.list_setup_sources(**params), - item_field="sources", - query=query, - ) - - def iter_wallet_transactions(self, **query: Any) -> AsyncIterator[Any]: + def iter_issue_events(self, issue_id: str, **query: Any) -> AsyncIterator[Any]: return aiter_cursor_pages( - lambda params: self.transactions.list_wallet_transactions(**params), - item_field="transactions", + lambda params: self.events.list_issue_events(issue_id, **params), + item_field="events", query=query, ) - - def iter_wallets(self, **query: Any) -> AsyncIterator[Any]: - return aiter_offset_pages( - lambda params: self.wallets.list_wallets(**params), item_field="wallets", query=query - ) diff --git a/src/reconify/models.py b/src/reconify/models.py index cb06b6d..f59f854 100644 --- a/src/reconify/models.py +++ b/src/reconify/models.py @@ -1,4 +1,4 @@ -"""Complete Pydantic v2 model surface derived from the Reconify OpenAPI schemas.""" +"""Pydantic models for the current Reconify public OpenAPI contract.""" from __future__ import annotations @@ -10,11 +10,13 @@ class TolerantStrEnum(str, Enum): + """String enum that preserves values added by a compatible API release.""" + @classmethod def _missing_(cls, value: object) -> TolerantStrEnum | None: if isinstance(value, str): member = str.__new__(cls, value) - member._name_ = f"UNKNOWN_{value.upper().replace('-', '_')}" + member._name_ = "UNKNOWN_" + value.upper().replace("-", "_").replace(".", "_") member._value_ = value cls._value2member_map_[value] = member return member @@ -29,1039 +31,217 @@ class RequestModel(BaseModel): model_config = ConfigDict(extra="forbid", populate_by_name=True, validate_assignment=True) -class AdjustmentRequestBodyAdjustmentType(TolerantStrEnum): - FEE = "fee" - REVERSAL = "reversal" - RECLASSIFICATION = "reclassification" - MANUAL_CORRECTION = "manual_correction" - WRITE_OFF = "write_off" - SPLIT_ALLOCATION = "split_allocation" +class Flow(TolerantStrEnum): + PAYMENT_TO_WALLET = "payment_to_wallet" + PAYMENT_TO_ORDER = "payment_to_order" + WALLET_TO_WALLET = "wallet_to_wallet" + WALLET_TO_PAYOUT = "wallet_to_payout" -class EventWindowBasis(TolerantStrEnum): - RECEIVED_AT = "received_at" - OCCURRED_AT = "occurred_at" +class EventType(TolerantStrEnum): + ORDER_FULFILLED = "order.fulfilled" + PAYMENT_FAILED = "payment.failed" + PAYMENT_INITIATED = "payment.initiated" + PAYMENT_SUCCEEDED = "payment.succeeded" + PAYOUT_FAILED = "payout.failed" + PAYOUT_INITIATED = "payout.initiated" + PAYOUT_SUCCEEDED = "payout.succeeded" + WALLET_CREDITED = "wallet.credited" + WALLET_DEBITED = "wallet.debited" + WALLET_REFUNDED = "wallet.refunded" -class IngestRowDirection(TolerantStrEnum): - DEBIT = "debit" - CREDIT = "credit" +class EntityType(TolerantStrEnum): + WALLET = "wallet" + ORDER = "order" -class IngestRowStatus(TolerantStrEnum): - PENDING = "pending" - POSTED = "posted" +class ReceiptStatus(TolerantStrEnum): + RECEIVED = "received" + PUBLISHED = "published" + PROCESSED = "processed" FAILED = "failed" - REVERSED = "reversed" - VOID = "void" - - -class PublicAdjustmentStatus(TolerantStrEnum): - PROPOSED = "proposed" - APPROVED = "approved" - APPLIED = "applied" - REVERSED = "reversed" - REJECTED = "rejected" - - -class PublicSignoffRole(TolerantStrEnum): - PREPARED_BY = "prepared_by" - REVIEWED_BY = "reviewed_by" - APPROVED_BY = "approved_by" - - -class ReconciliationScheduleFrequency(TolerantStrEnum): - ONE_TIME = "one_time" - DAILY = "daily" - WEEKLY = "weekly" - MONTHLY = "monthly" - - -class ReconciliationScheduleStatus(TolerantStrEnum): - ACTIVE = "active" - PAUSED = "paused" - - -class ReconciliationScheduleWindowBasis(TolerantStrEnum): - RECEIVED_AT = "received_at" - OCCURRED_AT = "occurred_at" - - -class ScheduleRequestBodyFrequency(TolerantStrEnum): - ONE_TIME = "one_time" - DAILY = "daily" - WEEKLY = "weekly" - MONTHLY = "monthly" - -class ScheduleRequestBodyStatus(TolerantStrEnum): - ACTIVE = "active" - PAUSED = "paused" +class IssueStatus(TolerantStrEnum): + OPEN = "open" + RESOLVED = "resolved" + RESOLVED_LATE = "resolved_late" -class ScheduleRequestBodyWindowBasis(TolerantStrEnum): - RECEIVED_AT = "received_at" - OCCURRED_AT = "occurred_at" +class IssueCategory(TolerantStrEnum): + BUSINESS_FAILURE = "business_failure" + MISSING_EVENT = "missing_event" + MISMATCH = "mismatch" + DUPLICATE_OR_CONFLICT = "duplicate_or_conflict" -class ScheduleUpdateRequestBodyFrequency(TolerantStrEnum): - ONE_TIME = "one_time" - DAILY = "daily" - WEEKLY = "weekly" - MONTHLY = "monthly" +class Severity(TolerantStrEnum): + HIGH = "high" + MEDIUM = "medium" -class ScheduleUpdateRequestBodyStatus(TolerantStrEnum): - ACTIVE = "active" - PAUSED = "paused" +class MemberRole(TolerantStrEnum): + OWNER = "owner" + ADMIN = "admin" + ANALYST = "analyst" + VIEWER = "viewer" -class ScheduleUpdateRequestBodyWindowBasis(TolerantStrEnum): - RECEIVED_AT = "received_at" - OCCURRED_AT = "occurred_at" - -class SourceRefRole(TolerantStrEnum): - LEFT = "left" - RIGHT = "right" - - -class TransactionDetailResourceStatus(TolerantStrEnum): - PENDING = "pending" - POSTED = "posted" - SUPERSEDED = "superseded" - REJECTED = "rejected" - - -class WalletTransactionStatus(TolerantStrEnum): - PENDING = "pending" - POSTED = "posted" - SUPERSEDED = "superseded" +class MonitoringResultStatus(TolerantStrEnum): + ACCEPTED = "accepted" + DUPLICATE = "duplicate" REJECTED = "rejected" -class AcceptedResult(ResponseModel): - index: int = Field(alias="index") - source_event_id: str = Field(alias="sourceEventId") - - -class Activity(ResponseModel): - action: str = Field(alias="action") - actor_user_id: str | None = Field(default=None, alias="actorUserId", exclude=False) - created_at: datetime = Field(alias="createdAt") - event_id: str | None = Field(default=None, alias="eventId", exclude=False) - finding_id: str | None = Field(default=None, alias="findingId", exclude=False) - id: str = Field(alias="id") - metadata: dict[str, Any] = Field(alias="metadata") - operation_id: str | None = Field(default=None, alias="operationId", exclude=False) - - -class AdjustmentOutputBody(ResponseModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - adjustment: PublicAdjustment = Field(alias="adjustment") - - -class AdjustmentRequestBody(RequestModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - adjustment_type: AdjustmentRequestBodyAdjustmentType = Field(alias="adjustmentType") - amount_minor: int | None = Field(default=None, alias="amountMinor", exclude=False) - currency: str | None = Field(default=None, alias="currency", exclude=False) - note: str | None = Field(default=None, alias="note", exclude=False) - resolution_reason: str | None = Field(default=None, alias="resolutionReason", exclude=False) - result_item_id: int | None = Field(default=None, alias="resultItemId", exclude=False) - - -class AlertLink(ResponseModel): - channel: str = Field(alias="channel") - created_at: datetime = Field(alias="createdAt") - finding_id: str = Field(alias="findingId") - id: str = Field(alias="id") - sent_at: datetime | None = Field(default=None, alias="sentAt", exclude=False) - status: str = Field(alias="status") - transition: str = Field(alias="transition") - +class MonitoringErrorCode(TolerantStrEnum): + INVALID_EVENT = "invalid_event" + UNKNOWN_FIELD = "unknown_field" + IDEMPOTENCY_CONFLICT = "idempotency_conflict" + DUPLICATE = "duplicate" + MALFORMED_REQUEST = "malformed_request" -class AlertRule(ResponseModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - breach_enabled: bool = Field(alias="breachEnabled") - channels: list[str] | None = Field(alias="channels") - control_id: str = Field(alias="controlId") - dedup_window_seconds: int = Field(alias="dedupWindowSeconds") - destinations: dict[str, Any] = Field(alias="destinations") - resolution_enabled: bool = Field(alias="resolutionEnabled") - severity_min: str = Field(alias="severityMin") - suppressed_until: datetime | None = Field(default=None, alias="suppressedUntil", exclude=False) - suppression_reason: str | None = Field(default=None, alias="suppressionReason", exclude=False) +class APIInfo(ResponseModel): + name: str + version: str + documentation_url: str + status_url: str -class AlertRulesOutputBody(ResponseModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - rules: list[AlertRule] | None = Field(alias="rules") +class Health(ResponseModel): + status: str -class BatchResponse(ResponseModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - accepted: list[AcceptedResult] | None = Field(alias="accepted") - rejected: list[RejectedResult] | None = Field(alias="rejected") +class MonitoringEventData(RequestModel): + provider: str | None = None + integration_ref: str | None = None + provider_transaction_id: str | None = None + provider_reference: str | None = None + failure_code: str | None = None + failure_message: str | None = None + retryable: bool | None = None -class CreateReconciliationInputBody(RequestModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - event_config: EventReconciliationConfig | None = Field( - default=None, alias="event_config", exclude=False - ) - ledger_config: LedgerConfig | None = Field(default=None, alias="ledger_config", exclude=False) - name: str | None = Field(default=None, alias="name", exclude=False) +class MonitoringEvent(RequestModel): + id: str | None = None + flow: Flow + type: EventType + reference: str + entity_id: str + occurred_at: datetime | None = None + amount: str | None = None + currency: str | None = None + data: MonitoringEventData | None = None + metadata: dict[str, str | int | float | bool] | None = None -class CreateSessionRequest(RequestModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - control_id: str = Field(alias="controlId") +class MonitoringBatchRequest(RequestModel): + events: list[MonitoringEvent] = Field(min_length=1, max_length=500) -class CreateSourceInputBody(RequestModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - name: str = Field(alias="name") - schema_mapping: SchemaMap | None = Field(default=None, alias="schemaMapping", exclude=False) +class MonitoringResult(ResponseModel): + index: int + status: MonitoringResultStatus + event_id: str | None = None + code: MonitoringErrorCode | None = None + field: str | None = None + message: str | None = None + warnings: list[str] | None = None -class CreateSourceRequest(RequestModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - config: dict[str, Any] | None = Field(default=None, alias="config", exclude=False) - name: str = Field(alias="name") - source_type: str = Field(alias="sourceType") +class MonitoringBatchResponse(ResponseModel): + results: list[MonitoringResult] -class DeliveriesOutputBody(ResponseModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - deliveries: list[Delivery] | None = Field(alias="deliveries") - -class Delivery(ResponseModel): - attempt_count: int = Field(alias="attemptCount") - channel: str = Field(alias="channel") - destination: str = Field(alias="destination") - id: str = Field(alias="id") - last_error_code: str | None = Field(default=None, alias="lastErrorCode", exclude=False) - next_attempt_at: datetime = Field(alias="nextAttemptAt") - sent_at: datetime | None = Field(default=None, alias="sentAt", exclude=False) - status: str = Field(alias="status") - transition: str = Field(alias="transition") - - -class ErrorDetail(ResponseModel): - location: str | None = Field(default=None, alias="location", exclude=False) - message: str | None = Field(default=None, alias="message", exclude=False) - value: Any | None = Field(default=None, alias="value", exclude=False) - - -class ErrorModel(ResponseModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - detail: str | None = Field(default=None, alias="detail", exclude=False) - errors: list[ErrorDetail] | None = Field(default=None, alias="errors", exclude=False) - instance: str | None = Field(default=None, alias="instance", exclude=False) - status: int | None = Field(default=None, alias="status", exclude=False) - title: str | None = Field(default=None, alias="title", exclude=False) - type: str | None = Field(default=None, alias="type", exclude=False) +class MonitoringSchema(ResponseModel): + limits: dict[str, int] | None = None + warnings: list[str] | None = None + error_codes: list[str] | None = None class Event(ResponseModel): - amount_minor: int = Field(alias="amountMinor") - applied: bool = Field(alias="applied") - balance_minor: int | None = Field(default=None, alias="balanceMinor", exclude=False) - canonical_hash: str = Field(alias="canonicalHash") - correlation_namespace: str | None = Field( - default=None, alias="correlationNamespace", exclude=False - ) - currency: str = Field(alias="currency") - entity_reference: str | None = Field(default=None, alias="entityReference", exclude=False) - event_schema_version: int = Field(alias="eventSchemaVersion") - event_type: str = Field(alias="eventType") - external_reference: str | None = Field(default=None, alias="externalReference", exclude=False) - finding_id: str | None = Field(default=None, alias="findingId", exclude=False) - id: str = Field(alias="id") - occurred_at: datetime = Field(alias="occurredAt") - operation_id: str | None = Field(default=None, alias="operationId", exclude=False) - operation_type: str | None = Field(default=None, alias="operationType", exclude=False) - processing_status: str = Field(alias="processingStatus") - provider_reference: str | None = Field(default=None, alias="providerReference", exclude=False) - received_at: datetime = Field(alias="receivedAt") - source_event_id: str = Field(alias="sourceEventId") - source_id: str = Field(alias="sourceId") - supersedes_event_id: str | None = Field(default=None, alias="supersedesEventId", exclude=False) - wallet_id: str | None = Field(default=None, alias="walletId", exclude=False) - - -class EventDetail(ResponseModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - event: Event = Field(alias="event") - metadata: dict[str, Any] = Field(alias="metadata") - payload: dict[str, Any] = Field(alias="payload") - timeline: list[EventTimelineItem] | None = Field(alias="timeline") - - -class EventPage(ResponseModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - events: list[Event] | None = Field(alias="events") - limit: int = Field(alias="limit") - next_cursor: str | None = Field(default=None, alias="nextCursor", exclude=False) - - -class EventReconciliationConfig(ResponseModel): - anchor_source_id: str = Field(alias="anchor_source_id") - correlation_namespace: str | None = Field( - default=None, alias="correlation_namespace", exclude=False - ) - source_ids: list[str] | None = Field(alias="source_ids") - window: EventWindow = Field(alias="window") - - -class EventRevealOutputBody(ResponseModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - field: str = Field(alias="field") - value: Any = Field(alias="value") - - -class EventTimelineItem(ResponseModel): - action: str = Field(alias="action") - created_at: datetime = Field(alias="createdAt") - kind: str = Field(alias="kind") - - -class EventWindow(ResponseModel): - basis: EventWindowBasis = Field(alias="basis") - end: str = Field(alias="end") - start: str = Field(alias="start") - - -class EvidenceOutputBody(ResponseModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - evidence: PublicEvidence = Field(alias="evidence") - - -class EvidenceRequestBody(RequestModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - checksum_sha256: str | None = Field(default=None, alias="checksumSha256", exclude=False) - entity_id: str = Field(alias="entityId") - entity_type: str = Field(alias="entityType") - evidence_type: str = Field(alias="evidenceType") - metadata: dict[str, Any] | None = Field(default=None, alias="metadata", exclude=False) - storage_path: str | None = Field(default=None, alias="storagePath", exclude=False) - - -class IngestError(ResponseModel): - idempotency_key: str | None = Field(default=None, alias="idempotencyKey", exclude=False) - index: int = Field(alias="index") - message: str = Field(alias="message") - - -class IngestEventsInputBody(RequestModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - events: list[PublicEvent] | None = Field(alias="events", min_length=1, max_length=500) - - -class IngestRow(RequestModel): - amount_minor: int = Field(alias="amountMinor") - currency: str = Field(alias="currency") - date: str = Field(alias="date") - direction: IngestRowDirection = Field(alias="direction") - idempotency_key: str = Field(alias="idempotencyKey") - metadata: Any | None = Field(default=None, alias="metadata", exclude=False) - name: str | None = Field(default=None, alias="name", exclude=False) - period_key: str | None = Field(default=None, alias="periodKey", exclude=False) - raw: Any | None = Field(default=None, alias="raw", exclude=False) - reference: str | None = Field(default=None, alias="reference", exclude=False) - status: IngestRowStatus | None = Field(default=None, alias="status", exclude=False) - transaction_type: str | None = Field(default=None, alias="transactionType", exclude=False) - value_date: str | None = Field(default=None, alias="valueDate", exclude=False) - - -class IngestTransactionsInputBody(RequestModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - transactions: list[IngestRow] | None = Field( - alias="transactions", min_length=1, max_length=5000 - ) - - -class IngestTransactionsOutputBody(ResponseModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - duplicates: int = Field(alias="duplicates") - errors: list[IngestError] | None = Field(default=None, alias="errors", exclude=False) - ingested: int = Field(alias="ingested") - - -class IntegritySource(ResponseModel): - id: str = Field(alias="id") - name: str = Field(alias="name") - source_type: str = Field(alias="sourceType") + id: str + flow: Flow + event_type: EventType + reference: str + entity_type: EntityType + entity_id: str + occurred_at: datetime + received_at: datetime + amount: str | None = None + currency: str | None = None + provider: str | None = None + status: ReceiptStatus + + +class ListEventsResponse(ResponseModel): + events: list[Event] + limit: int + next_cursor: str | None = None class Issue(ResponseModel): - affected_transaction_count: int | None = Field( - default=None, alias="affectedTransactionCount", exclude=False - ) - affected_wallet_count: int | None = Field( - default=None, alias="affectedWalletCount", exclude=False - ) - assigned_to: str | None = Field(default=None, alias="assignedTo", exclude=False) - control_id: str | None = Field(default=None, alias="controlId", exclude=False) - currency: str | None = Field(default=None, alias="currency", exclude=False) - event_id: str | None = Field(default=None, alias="eventId", exclude=False) - exposure_minor: int | None = Field(default=None, alias="exposureMinor", exclude=False) - id: str = Field(alias="id") - kind: str = Field(alias="kind") - opened_at: datetime = Field(alias="openedAt") - operation_id: str | None = Field(default=None, alias="operationId", exclude=False) - reason: str = Field(alias="reason") - resolved_at: datetime | None = Field(default=None, alias="resolvedAt", exclude=False) - severity: str = Field(alias="severity") - system_outcome: str | None = Field(default=None, alias="systemOutcome", exclude=False) - workflow_status: str = Field(alias="workflowStatus") - - -class IssueCounts(ResponseModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - acknowledged: int = Field(alias="acknowledged") - failed_delivery: int = Field(alias="failedDelivery") - investigating: int = Field(alias="investigating") - open: int = Field(alias="open") - resolved: int = Field(alias="resolved") - - -class IssueDetail(ResponseModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - activity: list[Activity] | None = Field(alias="activity") - deliveries: list[Delivery] | None = Field(alias="deliveries") - issue: Issue = Field(alias="issue") - notes: list[Note] | None = Field(alias="notes") + id: str + status: IssueStatus + category: IssueCategory + severity: Severity + message: str + assigned_to: str | None = None + operation_id: str | None = None + opened_at: datetime + resolved_at: datetime | None = None -class IssuePage(ResponseModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - issues: list[Issue] | None = Field(alias="issues") - limit: int = Field(alias="limit") - next_cursor: str | None = Field(default=None, alias="nextCursor", exclude=False) +class ListIssuesResponse(ResponseModel): + issues: list[Issue] + limit: int + next_cursor: str | None = None -class IssueUpdateInputBody(RequestModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - assigned_to: str | None = Field(default=None, alias="assignedTo", exclude=False) - workflow_status: str | None = Field(default=None, alias="workflowStatus", exclude=False) +class PatchIssueRequest(RequestModel): + assigned_to: str | None -class LedgerConfig(ResponseModel): - period_key: str = Field(alias="period_key") - sources: list[SourceRef] | None = Field(alias="sources") - - -class ListAdjustmentsOutputBody(ResponseModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - adjustments: list[PublicAdjustment] | None = Field(alias="adjustments") - limit: int = Field(alias="limit") - offset: int = Field(alias="offset") - total: int = Field(alias="total") - - -class ListEvidenceOutputBody(ResponseModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - evidence: list[PublicEvidence] | None = Field(alias="evidence") - - -class ListIntegritySourcesOutputBody(ResponseModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - sources: list[IntegritySource] | None = Field(alias="sources") - - -class ListPeriodsOutputBody(ResponseModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - periods: list[PeriodHealth] | None = Field(alias="periods") - +class Note(ResponseModel): + id: str | None = None + author_user_id: str | None = None + body: str + created_at: datetime | None = None -class ListReconciliationItemsOutputBody(ResponseModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - category: str = Field(alias="category") - limit: int = Field(alias="limit") - offset: int = Field(alias="offset") - rows: list[PublicResultItem] | None = Field(alias="rows") - total: int = Field(alias="total") +class AddNoteRequest(RequestModel): + body: str = Field(min_length=1, max_length=10_000) -class ListReconciliationsOutputBody(ResponseModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - limit: int = Field(alias="limit") - offset: int = Field(alias="offset") - reconciliations: list[Reconciliation] | None = Field(alias="reconciliations") - total: int = Field(alias="total") +class ListNotesResponse(ResponseModel): + notes: list[Note] -class ListSchedulesOutputBody(ResponseModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - limit: int = Field(alias="limit") - offset: int = Field(alias="offset") - schedules: list[ReconciliationSchedule] | None = Field(alias="schedules") - total: int = Field(alias="total") +class Organization(ResponseModel): + id: str + name: str + slug: str + created_at: datetime -class ListSignoffsOutputBody(ResponseModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - signoffs: list[PublicSignoff] | None = Field(alias="signoffs") +class Member(ResponseModel): + id: str + role: MemberRole + joined_at: datetime -class ListSourcesOutputBody(ResponseModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - limit: int = Field(alias="limit") - offset: int = Field(alias="offset") - sources: list[Source] | None = Field(alias="sources") - total: int = Field(alias="total") +class ListMembersResponse(ResponseModel): + members: list[Member] -class ListTransactionsOutputBody(ResponseModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - limit: int = Field(alias="limit") - next_cursor: str | None = Field(default=None, alias="nextCursor", exclude=False) - offset: int = Field(alias="offset") - total: int = Field(alias="total") - transactions: list[Transaction] | None = Field(alias="transactions") - -class Note(ResponseModel): - author_user_id: str = Field(alias="authorUserId") - body: str = Field(alias="body") - created_at: datetime = Field(alias="createdAt") - id: str = Field(alias="id") - - -class NoteInputBody(RequestModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - body: str = Field(alias="body") - - -class PeriodHealth(ResponseModel): - currency: str = Field(alias="currency") - has_run: bool = Field(alias="hasRun") - period_key: str = Field(alias="periodKey") - total_amount_minor: int = Field(alias="totalAmountMinor") - tx_count: int = Field(alias="txCount") - - -class PublicAdjustment(ResponseModel): - adjustment_type: str = Field(alias="adjustmentType") - amount_minor: int | None = Field(default=None, alias="amountMinor", exclude=False) - applied_at: datetime | None = Field(default=None, alias="appliedAt", exclude=False) - approved_at: datetime | None = Field(default=None, alias="approvedAt", exclude=False) - created_at: datetime = Field(alias="createdAt") - created_by_email: str | None = Field(default=None, alias="createdByEmail", exclude=False) - currency: str | None = Field(default=None, alias="currency", exclude=False) - id: str = Field(alias="id") - note: str = Field(alias="note") - reconciliation_id: str = Field(alias="reconciliationId") - rejected_at: datetime | None = Field(default=None, alias="rejectedAt", exclude=False) - resolution_reason: str = Field(alias="resolutionReason") - result_item_id: int | None = Field(default=None, alias="resultItemId", exclude=False) - reversed_at: datetime | None = Field(default=None, alias="reversedAt", exclude=False) - status: PublicAdjustmentStatus = Field(alias="status") - - -class PublicEvent(ResponseModel): - amount_minor: int = Field(alias="amountMinor") - balance_minor: int | None = Field(default=None, alias="balanceMinor", exclude=False) - correlation_namespace: str | None = Field( - default=None, alias="correlationNamespace", exclude=False - ) - currency: str = Field(alias="currency") - entity_reference: str | None = Field(default=None, alias="entityReference", exclude=False) - event_schema_version: int | None = Field( - default=None, alias="eventSchemaVersion", exclude=False - ) - event_type: str = Field(alias="eventType") - external_reference: str | None = Field(default=None, alias="externalReference", exclude=False) - metadata: dict[str, Any] | None = Field(default=None, alias="metadata", exclude=False) - occurred_at: datetime = Field(alias="occurredAt") - operation_id: str | None = Field(default=None, alias="operationId", exclude=False) - operation_type: str | None = Field(default=None, alias="operationType", exclude=False) - provider_reference: str | None = Field(default=None, alias="providerReference", exclude=False) - source_event_id: str = Field(alias="sourceEventId") - source_id: str = Field(alias="sourceId") - supersedes_source_event_id: str | None = Field( - default=None, alias="supersedesSourceEventId", exclude=False - ) - wallet_id: str | None = Field(default=None, alias="walletId", exclude=False) - - -class PublicEvidence(ResponseModel): - created_at: datetime = Field(alias="createdAt") - entity_id: str = Field(alias="entityId") - entity_type: str = Field(alias="entityType") - evidence_type: str = Field(alias="evidenceType") - id: str = Field(alias="id") - metadata: dict[str, Any] = Field(alias="metadata") - reconciliation_id: str = Field(alias="reconciliationId") - - -class PublicResultItem(ResponseModel): - amount_minor: int | None = Field(default=None, alias="amountMinor", exclude=False) - assigned_to: str | None = Field(default=None, alias="assignedTo", exclude=False) - category: str = Field(alias="category") - currency: str | None = Field(default=None, alias="currency", exclude=False) - exception_opened_at: str | None = Field(default=None, alias="exceptionOpenedAt", exclude=False) - exception_status: str = Field(alias="exceptionStatus") - id: int = Field(alias="id") - is_escalated: bool = Field(alias="isEscalated") - left_name: str | None = Field(default=None, alias="leftName", exclude=False) - note: str | None = Field(default=None, alias="note", exclude=False) - payload: dict[str, Any] = Field(alias="payload") - reason_code: str | None = Field(default=None, alias="reasonCode", exclude=False) - reference: str | None = Field(default=None, alias="reference", exclude=False) - review_status: str = Field(alias="reviewStatus") - right_name: str | None = Field(default=None, alias="rightName", exclude=False) - side: str | None = Field(default=None, alias="side", exclude=False) - type: str = Field(alias="type") - - -class PublicSignoff(ResponseModel): - approval_note: str = Field(alias="approvalNote") - id: str = Field(alias="id") - reconciliation_id: str = Field(alias="reconciliationId") - role: PublicSignoffRole = Field(alias="role") - signed_at: datetime = Field(alias="signedAt") - signed_with_exceptions: bool = Field(alias="signedWithExceptions") - signer_name: str = Field(alias="signerName") - signer_title: str = Field(alias="signerTitle") - - -class Reconciliation(ResponseModel): - completed_at: datetime | None = Field(default=None, alias="completedAt", exclude=False) - created_at: datetime = Field(alias="createdAt") - current_stage: str = Field(alias="currentStage") - id: str = Field(alias="id") - last_error_message: str | None = Field(default=None, alias="lastErrorMessage", exclude=False) - ledger_config: Any | None = Field(default=None, alias="ledgerConfig", exclude=False) - name: str | None = Field(default=None, alias="name", exclude=False) - org_id: str = Field(alias="orgId") - progress_pct: float = Field(alias="progressPct") - run_mode: str = Field(alias="runMode") - started_at: datetime | None = Field(default=None, alias="startedAt", exclude=False) - status: str = Field(alias="status") - updated_at: datetime = Field(alias="updatedAt") - - -class ReconciliationLink(ResponseModel): - category: str = Field(alias="category") - exception_status: str = Field(alias="exceptionStatus") - match_basis: str = Field(alias="matchBasis") - reconciliation_id: str = Field(alias="reconciliationId") - reconciliation_name: str | None = Field(default=None, alias="reconciliationName", exclude=False) - reference: str = Field(alias="reference") - result_item_id: int = Field(alias="resultItemId") - - -class ReconciliationOutputBody(ResponseModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - reconciliation: Reconciliation = Field(alias="reconciliation") - - -class ReconciliationSchedule(ResponseModel): - anchor_source_id: str = Field(alias="anchorSourceId") - correlation_namespace: str | None = Field( - default=None, alias="correlationNamespace", exclude=False - ) - created_at: datetime = Field(alias="createdAt") - frequency: ReconciliationScheduleFrequency = Field(alias="frequency") - id: str = Field(alias="id") - last_run_id: str | None = Field(default=None, alias="lastRunId", exclude=False) - next_run_at: datetime = Field(alias="nextRunAt") - rolling_window_duration: str = Field(alias="rollingWindowDuration") - source_ids: list[str] | None = Field(alias="sourceIds") - status: ReconciliationScheduleStatus = Field(alias="status") - updated_at: datetime = Field(alias="updatedAt") - window_basis: ReconciliationScheduleWindowBasis = Field(alias="windowBasis") - - -class RejectedResult(ResponseModel): - code: str = Field(alias="code") - index: int = Field(alias="index") - reason: str = Field(alias="reason") - - -class RelatedEvent(ResponseModel): - amount_minor: int = Field(alias="amountMinor") - currency: str = Field(alias="currency") - entity_reference: str | None = Field(default=None, alias="entityReference", exclude=False) - event_id: str = Field(alias="eventId") - event_type: str = Field(alias="eventType") - evidence_role: str = Field(alias="evidenceRole") - external_reference: str | None = Field(default=None, alias="externalReference", exclude=False) - occurred_at: datetime = Field(alias="occurredAt") - provider_reference: str | None = Field(default=None, alias="providerReference", exclude=False) - received_at: datetime = Field(alias="receivedAt") - source_event_id: str = Field(alias="sourceEventId") - source_id: str = Field(alias="sourceId") - superseded_by_event_id: str | None = Field( - default=None, alias="supersededByEventId", exclude=False - ) - - -class ResolveInputBody(RequestModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - evidence_attachment_id: str = Field(alias="evidenceAttachmentId") - reason: str = Field(alias="reason") - - -class ScheduleOutputBody(ResponseModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - schedule: ReconciliationSchedule = Field(alias="schedule") - - -class ScheduleRequestBody(RequestModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - anchor_source_id: str = Field(alias="anchor_source_id") - correlation_namespace: str | None = Field( - default=None, alias="correlation_namespace", exclude=False - ) - frequency: ScheduleRequestBodyFrequency = Field(alias="frequency") - next_run_at: datetime | None = Field(default=None, alias="next_run_at", exclude=False) - rolling_window_duration: str | None = Field( - default=None, alias="rolling_window_duration", exclude=False - ) - source_ids: list[str] | None = Field(alias="source_ids", min_length=2) - status: ScheduleRequestBodyStatus | None = Field(default=None, alias="status", exclude=False) - window_basis: ScheduleRequestBodyWindowBasis = Field(alias="window_basis") - - -class ScheduleUpdateRequestBody(RequestModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - anchor_source_id: str | None = Field(default=None, alias="anchor_source_id", exclude=False) - correlation_namespace: str | None = Field( - default=None, alias="correlation_namespace", exclude=False - ) - frequency: ScheduleUpdateRequestBodyFrequency | None = Field( - default=None, alias="frequency", exclude=False - ) - next_run_at: datetime | None = Field(default=None, alias="next_run_at", exclude=False) - rolling_window_duration: str | None = Field( - default=None, alias="rolling_window_duration", exclude=False - ) - source_ids: list[str] | None = Field(default=None, alias="source_ids", min_length=2) - status: ScheduleUpdateRequestBodyStatus | None = Field( - default=None, alias="status", exclude=False - ) - window_basis: ScheduleUpdateRequestBodyWindowBasis | None = Field( - default=None, alias="window_basis", exclude=False - ) - - -class SchemaMap(ResponseModel): - amount_col: str | None = Field(default=None, alias="amountCol", exclude=False) - currency_col: str | None = Field(default=None, alias="currencyCol", exclude=False) - date_col: str | None = Field(default=None, alias="dateCol", exclude=False) - date_layout: str | None = Field(default=None, alias="dateLayout", exclude=False) - decimal: str | None = Field(default=None, alias="decimal", exclude=False) - direction_col: str | None = Field(default=None, alias="directionCol", exclude=False) - name_col: str | None = Field(default=None, alias="nameCol", exclude=False) - ref_col: str | None = Field(default=None, alias="refCol", exclude=False) - thousands: str | None = Field(default=None, alias="thousands", exclude=False) - type_col: str | None = Field(default=None, alias="typeCol", exclude=False) - - -class SearchPage(ResponseModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - limit: int = Field(alias="limit") - results: list[SearchResult] | None = Field(alias="results") - - -class SearchResult(ResponseModel): - id: str = Field(alias="id") - kind: str = Field(alias="kind") - occurred_at: datetime = Field(alias="occurredAt") - status: str | None = Field(default=None, alias="status", exclude=False) - subtitle: str | None = Field(default=None, alias="subtitle", exclude=False) - title: str = Field(alias="title") - - -class SetupIntegration(ResponseModel): - capabilities: list[str] | None = Field(default=None, alias="capabilities", exclude=False) - created_at: datetime = Field(alias="createdAt") - health: str = Field(alias="health") - id: str = Field(alias="id") - name: str = Field(alias="name") - status: str = Field(alias="status") - type: str = Field(alias="type") - updated_at: datetime = Field(alias="updatedAt") - - -class SetupIntegrationOutputBody(ResponseModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - integration: SetupIntegration = Field(alias="integration") - - -class SetupIntegrationsOutputBody(ResponseModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - integrations: list[SetupIntegration] | None = Field(alias="integrations") - - -class SetupSessionDiff(ResponseModel): - missing: list[str] | None = Field(alias="missing") - unexpected: list[str] | None = Field(alias="unexpected") - - -class SetupSessionOutputBody(ResponseModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - test_session: SetupTestSession = Field(alias="testSession") - - -class SetupSource(ResponseModel): - config: dict[str, Any] | None = Field(default=None, alias="config", exclude=False) - created_at: datetime = Field(alias="createdAt") - id: str = Field(alias="id") - name: str = Field(alias="name") - org_id: str = Field(alias="orgId") - source_type: str = Field(alias="sourceType") - status: str = Field(alias="status") - updated_at: datetime = Field(alias="updatedAt") - - -class SetupSourceOutputBody(ResponseModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - source: SetupSource = Field(alias="source") - - -class SetupSourcesOutputBody(ResponseModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - limit: int = Field(alias="limit") - offset: int = Field(alias="offset") - sources: list[SetupSource] | None = Field(alias="sources") - total: int = Field(alias="total") - - -class SetupSubmitSessionInputBody(RequestModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - events: list[PublicEvent] | None = Field(alias="events") - - -class SetupTestSession(ResponseModel): - completed_at: datetime | None = Field(default=None, alias="completedAt", exclude=False) - control_id: str = Field(alias="controlId") - created_at: datetime = Field(alias="createdAt") - diff: SetupSessionDiff | None = Field(default=None, alias="diff", exclude=False) - expected_roles: list[str] | None = Field(alias="expectedRoles") - expires_at: datetime = Field(alias="expiresAt") - id: str = Field(alias="id") - instructions: dict[str, Any] | None = Field(default=None, alias="instructions", exclude=False) - observed_roles: list[str] | None = Field(alias="observedRoles") - org_id: str = Field(alias="orgId") - retry_count: int = Field(alias="retryCount") - status: str = Field(alias="status") - - -class SignoffOutputBody(ResponseModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - signoff: PublicSignoff = Field(alias="signoff") - - -class SignoffRequestBody(RequestModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - approval_note: str | None = Field(default=None, alias="approvalNote", exclude=False) - signed_with_exceptions: bool | None = Field( - default=None, alias="signedWithExceptions", exclude=False - ) - signer_name: str = Field(alias="signerName") - signer_title: str | None = Field(default=None, alias="signerTitle", exclude=False) - - -class Source(ResponseModel): - created_at: datetime = Field(alias="createdAt") - id: str = Field(alias="id") - name: str = Field(alias="name") - org_id: str = Field(alias="orgId") - schema_mapping: SchemaMap = Field(alias="schemaMapping") - updated_at: datetime = Field(alias="updatedAt") - - -class SourceOutputBody(ResponseModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - source: Source = Field(alias="source") - - -class SourceRef(ResponseModel): - display_name: str | None = Field(default=None, alias="display_name", exclude=False) - role: SourceRefRole = Field(alias="role") - source_id: str = Field(alias="source_id") - - -class StatusOutputBody(ResponseModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - status: str = Field(alias="status") - - -class Transaction(ResponseModel): - amount_minor: int = Field(alias="amountMinor") - currency: str = Field(alias="currency") - date: str = Field(alias="date") - direction: str = Field(alias="direction") - id: str = Field(alias="id") - idempotency_key: str = Field(alias="idempotencyKey") - ingested_at: datetime = Field(alias="ingestedAt") - metadata: Any | None = Field(default=None, alias="metadata", exclude=False) - name: str | None = Field(default=None, alias="name", exclude=False) - org_id: str = Field(alias="orgId") - period_key: str = Field(alias="periodKey") - raw: Any | None = Field(default=None, alias="raw", exclude=False) - reference: str | None = Field(default=None, alias="reference", exclude=False) - source_id: str = Field(alias="sourceId") - status: str = Field(alias="status") - transaction_type: str | None = Field(default=None, alias="transactionType", exclude=False) - value_date: str | None = Field(default=None, alias="valueDate", exclude=False) - - -class TransactionDetailResource(ResponseModel): - alerts: list[AlertLink] | None = Field(alias="alerts") - amount_minor: int = Field(alias="amountMinor") - correlation_namespace: str | None = Field( - default=None, alias="correlationNamespace", exclude=False - ) - currency: str = Field(alias="currency") - direction: str = Field(alias="direction") - entity_reference: str | None = Field(default=None, alias="entityReference", exclude=False) - event_type: str = Field(alias="eventType") - external_reference: str | None = Field(default=None, alias="externalReference", exclude=False) - finding_id: str | None = Field(default=None, alias="findingId", exclude=False) - findings: list[Issue] | None = Field(alias="findings") - id: str = Field(alias="id") - occurred_at: datetime = Field(alias="occurredAt") - operation_id: str | None = Field(default=None, alias="operationId", exclude=False) - provider_event_id: str | None = Field(default=None, alias="providerEventId", exclude=False) - provider_reference: str | None = Field(default=None, alias="providerReference", exclude=False) - received_at: datetime = Field(alias="receivedAt") - reconciliation_links: list[ReconciliationLink] | None = Field(alias="reconciliationLinks") - reconciliation_status: str | None = Field( - default=None, alias="reconciliationStatus", exclude=False - ) - related_events: list[RelatedEvent] | None = Field(alias="relatedEvents") - signed_amount_minor: int = Field(alias="signedAmountMinor") - source_event_id: str = Field(alias="sourceEventId") - status: TransactionDetailResourceStatus = Field(alias="status") - superseded: bool = Field(alias="superseded") - supersedes_source_event_id: str | None = Field( - default=None, alias="supersedesSourceEventId", exclude=False - ) - wallet_id: str = Field(alias="walletId") - - -class TransactionOutputBody(ResponseModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - transaction: TransactionDetailResource = Field(alias="transaction") - - -class TransactionPage(ResponseModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - limit: int = Field(alias="limit") - next_cursor: str | None = Field(default=None, alias="nextCursor", exclude=False) - transactions: list[WalletTransaction] | None = Field(alias="transactions") - - -class UpdateSourceInputBody(RequestModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - name: str | None = Field(default=None, alias="name", exclude=False) - schema_mapping: SchemaMap | None = Field(default=None, alias="schemaMapping", exclude=False) - - -class UpdateSourceRequest(RequestModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - config: dict[str, Any] | None = Field(default=None, alias="config", exclude=False) - name: str | None = Field(default=None, alias="name", exclude=False) - status: str | None = Field(default=None, alias="status", exclude=False) - - -class Wallet(ResponseModel): - balance_minor: int = Field(alias="balanceMinor") - balance_updated_at: datetime | None = Field( - default=None, alias="balanceUpdatedAt", exclude=False - ) - created_at: datetime = Field(alias="createdAt") - currency: str | None = Field(alias="currency") - event_count: int = Field(alias="eventCount") - has_additional_currencies: bool = Field(alias="hasAdditionalCurrencies") - id: str = Field(alias="id") - last_applied_at: datetime | None = Field(default=None, alias="lastAppliedAt", exclude=False) - name: str = Field(alias="name") - status: str = Field(alias="status") - updated_at: datetime = Field(alias="updatedAt") - - -class WalletBalance(ResponseModel): - balance_minor: int = Field(alias="balanceMinor") - currency: str | None = Field(alias="currency") - event_count: int = Field(alias="eventCount") - has_additional_currencies: bool = Field(alias="hasAdditionalCurrencies") - last_applied_at: datetime | None = Field(default=None, alias="lastAppliedAt", exclude=False) - updated_at: datetime | None = Field(default=None, alias="updatedAt", exclude=False) - wallet_id: str = Field(alias="walletId") - - -class WalletBalanceOutputBody(ResponseModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - balance: WalletBalance = Field(alias="balance") - - -class WalletOutputBody(ResponseModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - wallet: Wallet = Field(alias="wallet") - - -class WalletPage(ResponseModel): - schema_: str | None = Field(default=None, alias="$schema", exclude=True) - limit: int = Field(alias="limit") - offset: int = Field(alias="offset") - total: int = Field(alias="total") - wallets: list[Wallet] | None = Field(alias="wallets") - - -class WalletTransaction(ResponseModel): - amount_minor: int = Field(alias="amountMinor") - correlation_namespace: str | None = Field( - default=None, alias="correlationNamespace", exclude=False - ) - currency: str = Field(alias="currency") - direction: str = Field(alias="direction") - entity_reference: str | None = Field(default=None, alias="entityReference", exclude=False) - event_type: str = Field(alias="eventType") - external_reference: str | None = Field(default=None, alias="externalReference", exclude=False) - finding_id: str | None = Field(default=None, alias="findingId", exclude=False) - id: str = Field(alias="id") - occurred_at: datetime = Field(alias="occurredAt") - operation_id: str | None = Field(default=None, alias="operationId", exclude=False) - provider_event_id: str | None = Field(default=None, alias="providerEventId", exclude=False) - provider_reference: str | None = Field(default=None, alias="providerReference", exclude=False) - received_at: datetime = Field(alias="receivedAt") - reconciliation_status: str | None = Field( - default=None, alias="reconciliationStatus", exclude=False - ) - signed_amount_minor: int = Field(alias="signedAmountMinor") - source_event_id: str = Field(alias="sourceEventId") - status: WalletTransactionStatus = Field(alias="status") - superseded: bool = Field(alias="superseded") - supersedes_source_event_id: str | None = Field( - default=None, alias="supersedesSourceEventId", exclude=False - ) - wallet_id: str = Field(alias="walletId") - - -class AlertRuleRequest(RequestModel): - breach_enabled: bool = Field(alias="breachEnabled") - channels: list[str] | None = Field(default=None, alias="channels") - control_id: str = Field(alias="controlId") - dedup_window_seconds: int = Field(alias="dedupWindowSeconds") - destinations: dict[str, Any] = Field(alias="destinations") - resolution_enabled: bool = Field(alias="resolutionEnabled") - severity_min: str = Field(alias="severityMin") - suppressed_until: datetime | None = Field(default=None, alias="suppressedUntil") - suppression_reason: str | None = Field(default=None, alias="suppressionReason") +class Error(ResponseModel): + code: str | None = None + message: str | None = None + title: str | None = None + status: int | None = None + detail: str | None = None def model_dump(model: BaseModel) -> dict[str, Any]: - return model.model_dump(by_alias=True, exclude_none=True, exclude_unset=True) - - -for _model in list(globals().values()): - if isinstance(_model, type) and issubclass(_model, BaseModel) and _model is not BaseModel: - _model.model_rebuild() + return model.model_dump(mode="json", by_alias=True, exclude_none=True, exclude_unset=True) diff --git a/src/reconify/pagination.py b/src/reconify/pagination.py index 3e37e22..e4db356 100644 --- a/src/reconify/pagination.py +++ b/src/reconify/pagination.py @@ -14,7 +14,7 @@ def iter_cursor_pages( item_field: str, query: dict[str, Any] | None = None, ) -> Iterator[T]: - """Yield items while passing the server's opaque ``nextCursor`` onward.""" + """Yield items while passing the server's opaque ``next_cursor`` onward.""" params = dict(query or {}) params.pop("offset", None) if params.get("after") is not None else None diff --git a/src/reconify/resources/__init__.py b/src/reconify/resources/__init__.py index a1eaeea..7d8a841 100644 --- a/src/reconify/resources/__init__.py +++ b/src/reconify/resources/__init__.py @@ -1,123 +1,53 @@ -"""Resource clients grouped by public API surface.""" +"""Resource clients grouped by the current public API surface.""" -from .alerts import Alerts, AsyncAlerts from .events import AsyncEvents, Events from .ingestion import AsyncIngestion, Ingestion from .issues import AsyncIssues, Issues -from .ledger import AsyncLedger, Ledger -from .reconciliations import AsyncReconciliations, Reconciliations -from .search import AsyncSearch, Search -from .setup import AsyncSetup, Setup -from .transactions import AsyncTransactions, Transactions -from .wallets import AsyncWallets, Wallets +from .metadata import AsyncMetadata, Metadata +from .organization import AsyncOrganization, Organization SYNC_RESOURCE_CLASSES = { - "alerts": Alerts, + "metadata": Metadata, "events": Events, "ingestion": Ingestion, "issues": Issues, - "ledger": Ledger, - "reconciliations": Reconciliations, - "search": Search, - "setup": Setup, - "transactions": Transactions, - "wallets": Wallets, + "organization": Organization, } ASYNC_RESOURCE_CLASSES = { - "alerts": AsyncAlerts, + "metadata": AsyncMetadata, "events": AsyncEvents, "ingestion": AsyncIngestion, "issues": AsyncIssues, - "ledger": AsyncLedger, - "reconciliations": AsyncReconciliations, - "search": AsyncSearch, - "setup": AsyncSetup, - "transactions": AsyncTransactions, - "wallets": AsyncWallets, + "organization": AsyncOrganization, } OPERATION_SPECS = { - "list_alert_rules": ("alerts", "GET", "/alerts/rules"), - "put_alert_rule": ("alerts", "PUT", "/alerts/rules"), + "get_api_info": ("metadata", "GET", "/"), "list_events": ("events", "GET", "/events"), - "get_event": ("events", "GET", "/events/{id}"), - "reveal_event_field": ("events", "GET", "/events/{id}/reveal"), - "ingest_integrity_events": ("ingestion", "POST", "/integrity/events"), - "list_integrity_sources_for_reconciliation": ("reconciliations", "GET", "/integrity/sources"), - "ingest_integrity_test_events": ("ingestion", "POST", "/integrity/test-events"), + "ingest_monitoring_events": ("ingestion", "POST", "/events"), + "get_event": ("events", "GET", "/events/{event_id}"), + "get_health": ("metadata", "GET", "/health"), "list_issues": ("issues", "GET", "/issues"), - "get_issue_summary": ("issues", "GET", "/issues/summary"), - "get_issue": ("issues", "GET", "/issues/{id}"), - "update_issue": ("issues", "PATCH", "/issues/{id}"), - "list_issue_deliveries": ("issues", "GET", "/issues/{id}/deliveries"), - "retry_issue_delivery": ("issues", "POST", "/issues/{id}/deliveries/{deliveryId}/retry"), - "add_issue_note": ("issues", "POST", "/issues/{id}/notes"), - "resolve_issue": ("issues", "POST", "/issues/{id}/resolve"), - "list_ledger_sources": ("ledger", "GET", "/ledger/sources"), - "create_ledger_source": ("ledger", "POST", "/ledger/sources"), - "delete_ledger_source": ("ledger", "DELETE", "/ledger/sources/{id}"), - "get_ledger_source": ("ledger", "GET", "/ledger/sources/{id}"), - "update_ledger_source": ("ledger", "PATCH", "/ledger/sources/{id}"), - "list_source_periods": ("ledger", "GET", "/ledger/sources/{id}/periods"), - "list_transactions": ("ledger", "GET", "/ledger/sources/{id}/transactions"), - "ingest_transactions": ("ledger", "POST", "/ledger/sources/{id}/transactions"), - "list_reconciliation_schedules": ("reconciliations", "GET", "/reconciliation-schedules"), - "create_reconciliation_schedule": ("reconciliations", "POST", "/reconciliation-schedules"), - "delete_reconciliation_schedule": ( - "reconciliations", - "DELETE", - "/reconciliation-schedules/{id}", - ), - "get_reconciliation_schedule": ("reconciliations", "GET", "/reconciliation-schedules/{id}"), - "update_reconciliation_schedule": ( - "reconciliations", - "PATCH", - "/reconciliation-schedules/{id}", - ), - "list_reconciliations": ("reconciliations", "GET", "/reconciliations"), - "create_reconciliation": ("reconciliations", "POST", "/reconciliations"), - "get_reconciliation": ("reconciliations", "GET", "/reconciliations/{id}"), - "search_integrity_resources": ("search", "GET", "/search"), - "list_setup_integrations": ("setup", "GET", "/setup/integrations"), - "get_setup_integration": ("setup", "GET", "/setup/integrations/{id}"), - "list_setup_sources": ("setup", "GET", "/setup/sources"), - "create_setup_source": ("setup", "POST", "/setup/sources"), - "get_setup_source": ("setup", "GET", "/setup/sources/{id}"), - "update_setup_source": ("setup", "PATCH", "/setup/sources/{id}"), - "disable_setup_source": ("setup", "DELETE", "/setup/sources/{id}"), - "create_test_session": ("setup", "POST", "/setup/test-sessions"), - "get_test_session": ("setup", "GET", "/setup/test-sessions/{id}"), - "get_test_session_result": ("setup", "GET", "/setup/test-sessions/{id}/result"), - "retry_test_session": ("setup", "POST", "/setup/test-sessions/{id}/retry"), - "submit_test_session_events": ("setup", "POST", "/setup/test-sessions/{id}/submit"), - "list_wallet_transactions": ("transactions", "GET", "/transactions"), - "get_wallet_transaction": ("transactions", "GET", "/transactions/{id}"), - "list_wallets": ("wallets", "GET", "/wallets"), - "get_wallet": ("wallets", "GET", "/wallets/{id}"), - "get_wallet_balance": ("wallets", "GET", "/wallets/{id}/balance"), + "get_issue": ("issues", "GET", "/issues/{issue_id}"), + "update_issue": ("issues", "PATCH", "/issues/{issue_id}"), + "list_issue_events": ("events", "GET", "/issues/{issue_id}/events"), + "list_issue_notes": ("issues", "GET", "/issues/{issue_id}/notes"), + "add_issue_note": ("issues", "POST", "/issues/{issue_id}/notes"), + "get_organization": ("organization", "GET", "/organization"), + "list_organization_members": ("organization", "GET", "/organization/members"), } __all__ = [ - "Alerts", - "AsyncAlerts", "AsyncEvents", "AsyncIngestion", "AsyncIssues", - "AsyncLedger", - "AsyncReconciliations", - "AsyncSearch", - "AsyncSetup", - "AsyncTransactions", - "AsyncWallets", + "AsyncMetadata", + "AsyncOrganization", "Events", "Ingestion", "Issues", - "Ledger", - "Reconciliations", - "Search", - "Setup", - "Transactions", - "Wallets", + "Metadata", + "Organization", "ASYNC_RESOURCE_CLASSES", "OPERATION_SPECS", "SYNC_RESOURCE_CLASSES", diff --git a/src/reconify/resources/alerts.py b/src/reconify/resources/alerts.py deleted file mode 100644 index 48bed08..0000000 --- a/src/reconify/resources/alerts.py +++ /dev/null @@ -1,58 +0,0 @@ -"""Alerts API resource client.""" - -from __future__ import annotations - -from typing import Any - -from ..models import AlertRuleRequest, AlertRulesOutputBody, StatusOutputBody -from .base import AsyncResource, SyncResource - - -class Alerts(SyncResource): - def list_alert_rules(self, raw: bool = False, **query: Any) -> Any: - return self._request( - "GET", - "/alerts/rules", - params=query, - body=None, - response_model=AlertRulesOutputBody, - raw=raw, - ) - - def put_alert_rule(self, body: AlertRuleRequest, raw: bool = False, **query: Any) -> Any: - return self._request( - "PUT", - "/alerts/rules", - params=query, - body=body, - response_model=StatusOutputBody, - raw=raw, - ) - - -class AsyncAlerts(AsyncResource): - async def list_alert_rules(self, raw: bool = False, **query: Any) -> Any: - return await self._request( - "GET", - "/alerts/rules", - params=query, - body=None, - response_model=AlertRulesOutputBody, - raw=raw, - ) - - async def put_alert_rule(self, body: AlertRuleRequest, raw: bool = False, **query: Any) -> Any: - return await self._request( - "PUT", - "/alerts/rules", - params=query, - body=body, - response_model=StatusOutputBody, - raw=raw, - ) - - -OPERATION_SPECS = { - "list_alert_rules": ("alerts", "GET", "/alerts/rules"), - "put_alert_rule": ("alerts", "PUT", "/alerts/rules"), -} diff --git a/src/reconify/resources/events.py b/src/reconify/resources/events.py index eda0cb9..044e407 100644 --- a/src/reconify/resources/events.py +++ b/src/reconify/resources/events.py @@ -4,38 +4,31 @@ from typing import Any -from ..models import EventDetail, EventPage, EventRevealOutputBody +from ..models import Event, ListEventsResponse from .base import AsyncResource, SyncResource class Events(SyncResource): def list_events(self, raw: bool = False, **query: Any) -> Any: return self._request( - "GET", - "/events", - params=query, - body=None, - response_model=EventPage, - raw=raw, + "GET", "/events", params=query, response_model=ListEventsResponse, raw=raw ) - def get_event(self, id: str, raw: bool = False, **query: Any) -> Any: + def get_event(self, event_id: str, raw: bool = False, **query: Any) -> Any: return self._request( "GET", - "/events/{id}", - params={**query, "id": id}, - body=None, - response_model=EventDetail, + "/events/{event_id}", + params={**query, "event_id": event_id}, + response_model=Event, raw=raw, ) - def reveal_event_field(self, id: str, raw: bool = False, **query: Any) -> Any: + def list_issue_events(self, issue_id: str, raw: bool = False, **query: Any) -> Any: return self._request( "GET", - "/events/{id}/reveal", - params={**query, "id": id}, - body=None, - response_model=EventRevealOutputBody, + "/issues/{issue_id}/events", + params={**query, "issue_id": issue_id}, + response_model=ListEventsResponse, raw=raw, ) @@ -43,37 +36,23 @@ def reveal_event_field(self, id: str, raw: bool = False, **query: Any) -> Any: class AsyncEvents(AsyncResource): async def list_events(self, raw: bool = False, **query: Any) -> Any: return await self._request( - "GET", - "/events", - params=query, - body=None, - response_model=EventPage, - raw=raw, + "GET", "/events", params=query, response_model=ListEventsResponse, raw=raw ) - async def get_event(self, id: str, raw: bool = False, **query: Any) -> Any: + async def get_event(self, event_id: str, raw: bool = False, **query: Any) -> Any: return await self._request( "GET", - "/events/{id}", - params={**query, "id": id}, - body=None, - response_model=EventDetail, + "/events/{event_id}", + params={**query, "event_id": event_id}, + response_model=Event, raw=raw, ) - async def reveal_event_field(self, id: str, raw: bool = False, **query: Any) -> Any: + async def list_issue_events(self, issue_id: str, raw: bool = False, **query: Any) -> Any: return await self._request( "GET", - "/events/{id}/reveal", - params={**query, "id": id}, - body=None, - response_model=EventRevealOutputBody, + "/issues/{issue_id}/events", + params={**query, "issue_id": issue_id}, + response_model=ListEventsResponse, raw=raw, ) - - -OPERATION_SPECS = { - "list_events": ("events", "GET", "/events"), - "get_event": ("events", "GET", "/events/{id}"), - "reveal_event_field": ("events", "GET", "/events/{id}/reveal"), -} diff --git a/src/reconify/resources/ingestion.py b/src/reconify/resources/ingestion.py index 4877cef..d5e9517 100644 --- a/src/reconify/resources/ingestion.py +++ b/src/reconify/resources/ingestion.py @@ -1,94 +1,36 @@ -"""Ingestion API resource client.""" +"""Monitoring event ingestion resource client.""" from __future__ import annotations from typing import Any -from ..models import BatchResponse, IngestEventsInputBody +from ..models import MonitoringBatchRequest, MonitoringBatchResponse from .base import AsyncResource, SyncResource class Ingestion(SyncResource): - def ingest_integrity_events( - self, - body: IngestEventsInputBody, - integrity_test_session: str | None = None, - raw: bool = False, - **query: Any, + def ingest_monitoring_events( + self, body: MonitoringBatchRequest, raw: bool = False, **query: Any ) -> Any: return self._request( "POST", - "/integrity/events", + "/events", params=query, body=body, - response_model=BatchResponse, + response_model=MonitoringBatchResponse, raw=raw, - headers={"X-Integrity-Test-Session": integrity_test_session} - if integrity_test_session - else None, - ) - - def ingest_integrity_test_events( - self, - body: IngestEventsInputBody, - integrity_test_session: str | None = None, - raw: bool = False, - **query: Any, - ) -> Any: - return self._request( - "POST", - "/integrity/test-events", - params=query, - body=body, - response_model=BatchResponse, - raw=raw, - headers={"X-Integrity-Test-Session": integrity_test_session} - if integrity_test_session - else None, ) class AsyncIngestion(AsyncResource): - async def ingest_integrity_events( - self, - body: IngestEventsInputBody, - integrity_test_session: str | None = None, - raw: bool = False, - **query: Any, - ) -> Any: - return await self._request( - "POST", - "/integrity/events", - params=query, - body=body, - response_model=BatchResponse, - raw=raw, - headers={"X-Integrity-Test-Session": integrity_test_session} - if integrity_test_session - else None, - ) - - async def ingest_integrity_test_events( - self, - body: IngestEventsInputBody, - integrity_test_session: str | None = None, - raw: bool = False, - **query: Any, + async def ingest_monitoring_events( + self, body: MonitoringBatchRequest, raw: bool = False, **query: Any ) -> Any: return await self._request( "POST", - "/integrity/test-events", + "/events", params=query, body=body, - response_model=BatchResponse, + response_model=MonitoringBatchResponse, raw=raw, - headers={"X-Integrity-Test-Session": integrity_test_session} - if integrity_test_session - else None, ) - - -OPERATION_SPECS = { - "ingest_integrity_events": ("ingestion", "POST", "/integrity/events"), - "ingest_integrity_test_events": ("ingestion", "POST", "/integrity/test-events"), -} diff --git a/src/reconify/resources/issues.py b/src/reconify/resources/issues.py index 12f9252..3b637a2 100644 --- a/src/reconify/resources/issues.py +++ b/src/reconify/resources/issues.py @@ -1,18 +1,16 @@ -"""Issues API resource client.""" +"""Issue investigation resource client.""" from __future__ import annotations from typing import Any from ..models import ( - DeliveriesOutputBody, - IssueCounts, - IssueDetail, - IssuePage, - IssueUpdateInputBody, - NoteInputBody, - ResolveInputBody, - StatusOutputBody, + AddNoteRequest, + Issue, + ListIssuesResponse, + ListNotesResponse, + Note, + PatchIssueRequest, ) from .base import AsyncResource, SyncResource @@ -20,188 +18,112 @@ class Issues(SyncResource): def list_issues(self, raw: bool = False, **query: Any) -> Any: return self._request( - "GET", - "/issues", - params=query, - body=None, - response_model=IssuePage, - raw=raw, + "GET", "/issues", params=query, response_model=ListIssuesResponse, raw=raw ) - def get_issue_summary(self, raw: bool = False, **query: Any) -> Any: + def get_issue(self, issue_id: str, raw: bool = False, **query: Any) -> Any: return self._request( "GET", - "/issues/summary", - params=query, - body=None, - response_model=IssueCounts, - raw=raw, - ) - - def get_issue(self, id: str, raw: bool = False, **query: Any) -> Any: - return self._request( - "GET", - "/issues/{id}", - params={**query, "id": id}, - body=None, - response_model=IssueDetail, + "/issues/{issue_id}", + params={**query, "issue_id": issue_id}, + response_model=Issue, raw=raw, ) def update_issue( - self, id: str, body: IssueUpdateInputBody, raw: bool = False, **query: Any + self, issue_id: str, body: PatchIssueRequest, raw: bool = False, **query: Any ) -> Any: return self._request( "PATCH", - "/issues/{id}", - params={**query, "id": id}, + "/issues/{issue_id}", + params={**query, "issue_id": issue_id}, body=body, - response_model=StatusOutputBody, + response_model=Issue, raw=raw, ) - def list_issue_deliveries(self, id: str, raw: bool = False, **query: Any) -> Any: + def list_issue_notes(self, issue_id: str, raw: bool = False, **query: Any) -> Any: return self._request( "GET", - "/issues/{id}/deliveries", - params={**query, "id": id}, - body=None, - response_model=DeliveriesOutputBody, + "/issues/{issue_id}/notes", + params={**query, "issue_id": issue_id}, + response_model=ListNotesResponse, raw=raw, ) - def retry_issue_delivery( - self, id: str, delivery_id: str, raw: bool = False, **query: Any + def add_issue_note( + self, + issue_id: str, + body: AddNoteRequest, + *, + idempotency_key: str | None = None, + raw: bool = False, + **query: Any, ) -> Any: + headers = {"Idempotency-Key": idempotency_key} if idempotency_key else None return self._request( "POST", - "/issues/{id}/deliveries/{deliveryId}/retry", - params={**query, "id": id, "delivery_id": delivery_id}, - body=None, - response_model=StatusOutputBody, - raw=raw, - ) - - def add_issue_note(self, id: str, body: NoteInputBody, raw: bool = False, **query: Any) -> Any: - return self._request( - "POST", - "/issues/{id}/notes", - params={**query, "id": id}, - body=body, - response_model=StatusOutputBody, - raw=raw, - ) - - def resolve_issue( - self, id: str, body: ResolveInputBody, raw: bool = False, **query: Any - ) -> Any: - return self._request( - "POST", - "/issues/{id}/resolve", - params={**query, "id": id}, + "/issues/{issue_id}/notes", + params={**query, "issue_id": issue_id}, body=body, - response_model=StatusOutputBody, + response_model=Note, raw=raw, + headers=headers, ) class AsyncIssues(AsyncResource): async def list_issues(self, raw: bool = False, **query: Any) -> Any: return await self._request( - "GET", - "/issues", - params=query, - body=None, - response_model=IssuePage, - raw=raw, + "GET", "/issues", params=query, response_model=ListIssuesResponse, raw=raw ) - async def get_issue_summary(self, raw: bool = False, **query: Any) -> Any: + async def get_issue(self, issue_id: str, raw: bool = False, **query: Any) -> Any: return await self._request( "GET", - "/issues/summary", - params=query, - body=None, - response_model=IssueCounts, - raw=raw, - ) - - async def get_issue(self, id: str, raw: bool = False, **query: Any) -> Any: - return await self._request( - "GET", - "/issues/{id}", - params={**query, "id": id}, - body=None, - response_model=IssueDetail, + "/issues/{issue_id}", + params={**query, "issue_id": issue_id}, + response_model=Issue, raw=raw, ) async def update_issue( - self, id: str, body: IssueUpdateInputBody, raw: bool = False, **query: Any + self, issue_id: str, body: PatchIssueRequest, raw: bool = False, **query: Any ) -> Any: return await self._request( "PATCH", - "/issues/{id}", - params={**query, "id": id}, + "/issues/{issue_id}", + params={**query, "issue_id": issue_id}, body=body, - response_model=StatusOutputBody, + response_model=Issue, raw=raw, ) - async def list_issue_deliveries(self, id: str, raw: bool = False, **query: Any) -> Any: + async def list_issue_notes(self, issue_id: str, raw: bool = False, **query: Any) -> Any: return await self._request( "GET", - "/issues/{id}/deliveries", - params={**query, "id": id}, - body=None, - response_model=DeliveriesOutputBody, - raw=raw, - ) - - async def retry_issue_delivery( - self, id: str, delivery_id: str, raw: bool = False, **query: Any - ) -> Any: - return await self._request( - "POST", - "/issues/{id}/deliveries/{deliveryId}/retry", - params={**query, "id": id, "delivery_id": delivery_id}, - body=None, - response_model=StatusOutputBody, + "/issues/{issue_id}/notes", + params={**query, "issue_id": issue_id}, + response_model=ListNotesResponse, raw=raw, ) async def add_issue_note( - self, id: str, body: NoteInputBody, raw: bool = False, **query: Any - ) -> Any: - return await self._request( - "POST", - "/issues/{id}/notes", - params={**query, "id": id}, - body=body, - response_model=StatusOutputBody, - raw=raw, - ) - - async def resolve_issue( - self, id: str, body: ResolveInputBody, raw: bool = False, **query: Any + self, + issue_id: str, + body: AddNoteRequest, + *, + idempotency_key: str | None = None, + raw: bool = False, + **query: Any, ) -> Any: + headers = {"Idempotency-Key": idempotency_key} if idempotency_key else None return await self._request( "POST", - "/issues/{id}/resolve", - params={**query, "id": id}, + "/issues/{issue_id}/notes", + params={**query, "issue_id": issue_id}, body=body, - response_model=StatusOutputBody, + response_model=Note, raw=raw, + headers=headers, ) - - -OPERATION_SPECS = { - "list_issues": ("issues", "GET", "/issues"), - "get_issue_summary": ("issues", "GET", "/issues/summary"), - "get_issue": ("issues", "GET", "/issues/{id}"), - "update_issue": ("issues", "PATCH", "/issues/{id}"), - "list_issue_deliveries": ("issues", "GET", "/issues/{id}/deliveries"), - "retry_issue_delivery": ("issues", "POST", "/issues/{id}/deliveries/{deliveryId}/retry"), - "add_issue_note": ("issues", "POST", "/issues/{id}/notes"), - "resolve_issue": ("issues", "POST", "/issues/{id}/resolve"), -} diff --git a/src/reconify/resources/ledger.py b/src/reconify/resources/ledger.py deleted file mode 100644 index 2b71d78..0000000 --- a/src/reconify/resources/ledger.py +++ /dev/null @@ -1,205 +0,0 @@ -"""Ledger API resource client.""" - -from __future__ import annotations - -from typing import Any - -from ..models import ( - CreateSourceInputBody, - IngestTransactionsInputBody, - IngestTransactionsOutputBody, - ListPeriodsOutputBody, - ListSourcesOutputBody, - ListTransactionsOutputBody, - SourceOutputBody, - UpdateSourceInputBody, -) -from .base import AsyncResource, SyncResource - - -class Ledger(SyncResource): - def list_ledger_sources(self, raw: bool = False, **query: Any) -> Any: - return self._request( - "GET", - "/ledger/sources", - params=query, - body=None, - response_model=ListSourcesOutputBody, - raw=raw, - ) - - def create_ledger_source( - self, body: CreateSourceInputBody, raw: bool = False, **query: Any - ) -> Any: - return self._request( - "POST", - "/ledger/sources", - params=query, - body=body, - response_model=SourceOutputBody, - raw=raw, - ) - - def delete_ledger_source(self, id: str, raw: bool = False, **query: Any) -> Any: - return self._request( - "DELETE", - "/ledger/sources/{id}", - params={**query, "id": id}, - body=None, - response_model=None, - raw=raw, - ) - - def get_ledger_source(self, id: str, raw: bool = False, **query: Any) -> Any: - return self._request( - "GET", - "/ledger/sources/{id}", - params={**query, "id": id}, - body=None, - response_model=SourceOutputBody, - raw=raw, - ) - - def update_ledger_source( - self, id: str, body: UpdateSourceInputBody, raw: bool = False, **query: Any - ) -> Any: - return self._request( - "PATCH", - "/ledger/sources/{id}", - params={**query, "id": id}, - body=body, - response_model=SourceOutputBody, - raw=raw, - ) - - def list_source_periods(self, id: str, raw: bool = False, **query: Any) -> Any: - return self._request( - "GET", - "/ledger/sources/{id}/periods", - params={**query, "id": id}, - body=None, - response_model=ListPeriodsOutputBody, - raw=raw, - ) - - def list_transactions(self, id: str, raw: bool = False, **query: Any) -> Any: - return self._request( - "GET", - "/ledger/sources/{id}/transactions", - params={**query, "id": id}, - body=None, - response_model=ListTransactionsOutputBody, - raw=raw, - ) - - def ingest_transactions( - self, id: str, body: IngestTransactionsInputBody, raw: bool = False, **query: Any - ) -> Any: - return self._request( - "POST", - "/ledger/sources/{id}/transactions", - params={**query, "id": id}, - body=body, - response_model=IngestTransactionsOutputBody, - raw=raw, - ) - - -class AsyncLedger(AsyncResource): - async def list_ledger_sources(self, raw: bool = False, **query: Any) -> Any: - return await self._request( - "GET", - "/ledger/sources", - params=query, - body=None, - response_model=ListSourcesOutputBody, - raw=raw, - ) - - async def create_ledger_source( - self, body: CreateSourceInputBody, raw: bool = False, **query: Any - ) -> Any: - return await self._request( - "POST", - "/ledger/sources", - params=query, - body=body, - response_model=SourceOutputBody, - raw=raw, - ) - - async def delete_ledger_source(self, id: str, raw: bool = False, **query: Any) -> Any: - return await self._request( - "DELETE", - "/ledger/sources/{id}", - params={**query, "id": id}, - body=None, - response_model=None, - raw=raw, - ) - - async def get_ledger_source(self, id: str, raw: bool = False, **query: Any) -> Any: - return await self._request( - "GET", - "/ledger/sources/{id}", - params={**query, "id": id}, - body=None, - response_model=SourceOutputBody, - raw=raw, - ) - - async def update_ledger_source( - self, id: str, body: UpdateSourceInputBody, raw: bool = False, **query: Any - ) -> Any: - return await self._request( - "PATCH", - "/ledger/sources/{id}", - params={**query, "id": id}, - body=body, - response_model=SourceOutputBody, - raw=raw, - ) - - async def list_source_periods(self, id: str, raw: bool = False, **query: Any) -> Any: - return await self._request( - "GET", - "/ledger/sources/{id}/periods", - params={**query, "id": id}, - body=None, - response_model=ListPeriodsOutputBody, - raw=raw, - ) - - async def list_transactions(self, id: str, raw: bool = False, **query: Any) -> Any: - return await self._request( - "GET", - "/ledger/sources/{id}/transactions", - params={**query, "id": id}, - body=None, - response_model=ListTransactionsOutputBody, - raw=raw, - ) - - async def ingest_transactions( - self, id: str, body: IngestTransactionsInputBody, raw: bool = False, **query: Any - ) -> Any: - return await self._request( - "POST", - "/ledger/sources/{id}/transactions", - params={**query, "id": id}, - body=body, - response_model=IngestTransactionsOutputBody, - raw=raw, - ) - - -OPERATION_SPECS = { - "list_ledger_sources": ("ledger", "GET", "/ledger/sources"), - "create_ledger_source": ("ledger", "POST", "/ledger/sources"), - "delete_ledger_source": ("ledger", "DELETE", "/ledger/sources/{id}"), - "get_ledger_source": ("ledger", "GET", "/ledger/sources/{id}"), - "update_ledger_source": ("ledger", "PATCH", "/ledger/sources/{id}"), - "list_source_periods": ("ledger", "GET", "/ledger/sources/{id}/periods"), - "list_transactions": ("ledger", "GET", "/ledger/sources/{id}/transactions"), - "ingest_transactions": ("ledger", "POST", "/ledger/sources/{id}/transactions"), -} diff --git a/src/reconify/resources/metadata.py b/src/reconify/resources/metadata.py new file mode 100644 index 0000000..fcb25dd --- /dev/null +++ b/src/reconify/resources/metadata.py @@ -0,0 +1,24 @@ +"""Public API metadata resource.""" + +from __future__ import annotations + +from typing import Any + +from ..models import APIInfo, Health +from .base import AsyncResource, SyncResource + + +class Metadata(SyncResource): + def get_api_info(self, raw: bool = False, **query: Any) -> Any: + return self._request("GET", "", params=query, response_model=APIInfo, raw=raw) + + def get_health(self, raw: bool = False, **query: Any) -> Any: + return self._request("GET", "/health", params=query, response_model=Health, raw=raw) + + +class AsyncMetadata(AsyncResource): + async def get_api_info(self, raw: bool = False, **query: Any) -> Any: + return await self._request("GET", "", params=query, response_model=APIInfo, raw=raw) + + async def get_health(self, raw: bool = False, **query: Any) -> Any: + return await self._request("GET", "/health", params=query, response_model=Health, raw=raw) diff --git a/src/reconify/resources/organization.py b/src/reconify/resources/organization.py new file mode 100644 index 0000000..c355aa5 --- /dev/null +++ b/src/reconify/resources/organization.py @@ -0,0 +1,41 @@ +"""Organization resource client.""" + +from __future__ import annotations + +from typing import Any + +from ..models import ListMembersResponse +from ..models import Organization as OrganizationModel +from .base import AsyncResource, SyncResource + + +class Organization(SyncResource): + def get_organization(self, raw: bool = False, **query: Any) -> Any: + return self._request( + "GET", "/organization", params=query, response_model=OrganizationModel, raw=raw + ) + + def list_organization_members(self, raw: bool = False, **query: Any) -> Any: + return self._request( + "GET", + "/organization/members", + params=query, + response_model=ListMembersResponse, + raw=raw, + ) + + +class AsyncOrganization(AsyncResource): + async def get_organization(self, raw: bool = False, **query: Any) -> Any: + return await self._request( + "GET", "/organization", params=query, response_model=OrganizationModel, raw=raw + ) + + async def list_organization_members(self, raw: bool = False, **query: Any) -> Any: + return await self._request( + "GET", + "/organization/members", + params=query, + response_model=ListMembersResponse, + raw=raw, + ) diff --git a/src/reconify/resources/reconciliations.py b/src/reconify/resources/reconciliations.py deleted file mode 100644 index 6db998a..0000000 --- a/src/reconify/resources/reconciliations.py +++ /dev/null @@ -1,236 +0,0 @@ -"""Reconciliations API resource client.""" - -from __future__ import annotations - -from typing import Any - -from ..models import ( - CreateReconciliationInputBody, - ListIntegritySourcesOutputBody, - ListReconciliationsOutputBody, - ListSchedulesOutputBody, - ReconciliationOutputBody, - ScheduleOutputBody, - ScheduleRequestBody, - ScheduleUpdateRequestBody, -) -from .base import AsyncResource, SyncResource - - -class Reconciliations(SyncResource): - def list_integrity_sources_for_reconciliation(self, raw: bool = False, **query: Any) -> Any: - return self._request( - "GET", - "/integrity/sources", - params=query, - body=None, - response_model=ListIntegritySourcesOutputBody, - raw=raw, - ) - - def list_reconciliation_schedules(self, raw: bool = False, **query: Any) -> Any: - return self._request( - "GET", - "/reconciliation-schedules", - params=query, - body=None, - response_model=ListSchedulesOutputBody, - raw=raw, - ) - - def create_reconciliation_schedule( - self, body: ScheduleRequestBody, raw: bool = False, **query: Any - ) -> Any: - return self._request( - "POST", - "/reconciliation-schedules", - params=query, - body=body, - response_model=ScheduleOutputBody, - raw=raw, - ) - - def delete_reconciliation_schedule(self, id: str, raw: bool = False, **query: Any) -> Any: - return self._request( - "DELETE", - "/reconciliation-schedules/{id}", - params={**query, "id": id}, - body=None, - response_model=None, - raw=raw, - ) - - def get_reconciliation_schedule(self, id: str, raw: bool = False, **query: Any) -> Any: - return self._request( - "GET", - "/reconciliation-schedules/{id}", - params={**query, "id": id}, - body=None, - response_model=ScheduleOutputBody, - raw=raw, - ) - - def update_reconciliation_schedule( - self, id: str, body: ScheduleUpdateRequestBody, raw: bool = False, **query: Any - ) -> Any: - return self._request( - "PATCH", - "/reconciliation-schedules/{id}", - params={**query, "id": id}, - body=body, - response_model=ScheduleOutputBody, - raw=raw, - ) - - def list_reconciliations(self, raw: bool = False, **query: Any) -> Any: - return self._request( - "GET", - "/reconciliations", - params=query, - body=None, - response_model=ListReconciliationsOutputBody, - raw=raw, - ) - - def create_reconciliation( - self, body: CreateReconciliationInputBody, raw: bool = False, **query: Any - ) -> Any: - return self._request( - "POST", - "/reconciliations", - params=query, - body=body, - response_model=ReconciliationOutputBody, - raw=raw, - ) - - def get_reconciliation(self, id: str, raw: bool = False, **query: Any) -> Any: - return self._request( - "GET", - "/reconciliations/{id}", - params={**query, "id": id}, - body=None, - response_model=ReconciliationOutputBody, - raw=raw, - ) - - -class AsyncReconciliations(AsyncResource): - async def list_integrity_sources_for_reconciliation( - self, raw: bool = False, **query: Any - ) -> Any: - return await self._request( - "GET", - "/integrity/sources", - params=query, - body=None, - response_model=ListIntegritySourcesOutputBody, - raw=raw, - ) - - async def list_reconciliation_schedules(self, raw: bool = False, **query: Any) -> Any: - return await self._request( - "GET", - "/reconciliation-schedules", - params=query, - body=None, - response_model=ListSchedulesOutputBody, - raw=raw, - ) - - async def create_reconciliation_schedule( - self, body: ScheduleRequestBody, raw: bool = False, **query: Any - ) -> Any: - return await self._request( - "POST", - "/reconciliation-schedules", - params=query, - body=body, - response_model=ScheduleOutputBody, - raw=raw, - ) - - async def delete_reconciliation_schedule(self, id: str, raw: bool = False, **query: Any) -> Any: - return await self._request( - "DELETE", - "/reconciliation-schedules/{id}", - params={**query, "id": id}, - body=None, - response_model=None, - raw=raw, - ) - - async def get_reconciliation_schedule(self, id: str, raw: bool = False, **query: Any) -> Any: - return await self._request( - "GET", - "/reconciliation-schedules/{id}", - params={**query, "id": id}, - body=None, - response_model=ScheduleOutputBody, - raw=raw, - ) - - async def update_reconciliation_schedule( - self, id: str, body: ScheduleUpdateRequestBody, raw: bool = False, **query: Any - ) -> Any: - return await self._request( - "PATCH", - "/reconciliation-schedules/{id}", - params={**query, "id": id}, - body=body, - response_model=ScheduleOutputBody, - raw=raw, - ) - - async def list_reconciliations(self, raw: bool = False, **query: Any) -> Any: - return await self._request( - "GET", - "/reconciliations", - params=query, - body=None, - response_model=ListReconciliationsOutputBody, - raw=raw, - ) - - async def create_reconciliation( - self, body: CreateReconciliationInputBody, raw: bool = False, **query: Any - ) -> Any: - return await self._request( - "POST", - "/reconciliations", - params=query, - body=body, - response_model=ReconciliationOutputBody, - raw=raw, - ) - - async def get_reconciliation(self, id: str, raw: bool = False, **query: Any) -> Any: - return await self._request( - "GET", - "/reconciliations/{id}", - params={**query, "id": id}, - body=None, - response_model=ReconciliationOutputBody, - raw=raw, - ) - - -OPERATION_SPECS = { - "list_integrity_sources_for_reconciliation": ("reconciliations", "GET", "/integrity/sources"), - "list_reconciliation_schedules": ("reconciliations", "GET", "/reconciliation-schedules"), - "create_reconciliation_schedule": ("reconciliations", "POST", "/reconciliation-schedules"), - "delete_reconciliation_schedule": ( - "reconciliations", - "DELETE", - "/reconciliation-schedules/{id}", - ), - "get_reconciliation_schedule": ("reconciliations", "GET", "/reconciliation-schedules/{id}"), - "update_reconciliation_schedule": ( - "reconciliations", - "PATCH", - "/reconciliation-schedules/{id}", - ), - "list_reconciliations": ("reconciliations", "GET", "/reconciliations"), - "create_reconciliation": ("reconciliations", "POST", "/reconciliations"), - "get_reconciliation": ("reconciliations", "GET", "/reconciliations/{id}"), -} diff --git a/src/reconify/resources/search.py b/src/reconify/resources/search.py deleted file mode 100644 index 4df46b1..0000000 --- a/src/reconify/resources/search.py +++ /dev/null @@ -1,37 +0,0 @@ -"""Search API resource client.""" - -from __future__ import annotations - -from typing import Any - -from ..models import SearchPage -from .base import AsyncResource, SyncResource - - -class Search(SyncResource): - def search_integrity_resources(self, raw: bool = False, **query: Any) -> Any: - return self._request( - "GET", - "/search", - params=query, - body=None, - response_model=SearchPage, - raw=raw, - ) - - -class AsyncSearch(AsyncResource): - async def search_integrity_resources(self, raw: bool = False, **query: Any) -> Any: - return await self._request( - "GET", - "/search", - params=query, - body=None, - response_model=SearchPage, - raw=raw, - ) - - -OPERATION_SPECS = { - "search_integrity_resources": ("search", "GET", "/search"), -} diff --git a/src/reconify/resources/setup.py b/src/reconify/resources/setup.py deleted file mode 100644 index f83567d..0000000 --- a/src/reconify/resources/setup.py +++ /dev/null @@ -1,311 +0,0 @@ -"""Setup API resource client.""" - -from __future__ import annotations - -from typing import Any - -from ..models import ( - BatchResponse, - CreateSessionRequest, - CreateSourceRequest, - SetupIntegrationOutputBody, - SetupIntegrationsOutputBody, - SetupSessionOutputBody, - SetupSourceOutputBody, - SetupSourcesOutputBody, - SetupSubmitSessionInputBody, - UpdateSourceRequest, -) -from .base import AsyncResource, SyncResource - - -class Setup(SyncResource): - def list_setup_integrations(self, raw: bool = False, **query: Any) -> Any: - return self._request( - "GET", - "/setup/integrations", - params=query, - body=None, - response_model=SetupIntegrationsOutputBody, - raw=raw, - ) - - def get_setup_integration(self, id: str, raw: bool = False, **query: Any) -> Any: - return self._request( - "GET", - "/setup/integrations/{id}", - params={**query, "id": id}, - body=None, - response_model=SetupIntegrationOutputBody, - raw=raw, - ) - - def list_setup_sources(self, raw: bool = False, **query: Any) -> Any: - return self._request( - "GET", - "/setup/sources", - params=query, - body=None, - response_model=SetupSourcesOutputBody, - raw=raw, - ) - - def create_setup_source( - self, body: CreateSourceRequest, raw: bool = False, **query: Any - ) -> Any: - return self._request( - "POST", - "/setup/sources", - params=query, - body=body, - response_model=SetupSourceOutputBody, - raw=raw, - ) - - def get_setup_source(self, id: str, raw: bool = False, **query: Any) -> Any: - return self._request( - "GET", - "/setup/sources/{id}", - params={**query, "id": id}, - body=None, - response_model=SetupSourceOutputBody, - raw=raw, - ) - - def update_setup_source( - self, id: str, body: UpdateSourceRequest, raw: bool = False, **query: Any - ) -> Any: - return self._request( - "PATCH", - "/setup/sources/{id}", - params={**query, "id": id}, - body=body, - response_model=SetupSourceOutputBody, - raw=raw, - ) - - def disable_setup_source(self, id: str, raw: bool = False, **query: Any) -> Any: - return self._request( - "DELETE", - "/setup/sources/{id}", - params={**query, "id": id}, - body=None, - response_model=None, - raw=raw, - ) - - def create_test_session( - self, body: CreateSessionRequest, raw: bool = False, **query: Any - ) -> Any: - return self._request( - "POST", - "/setup/test-sessions", - params=query, - body=body, - response_model=SetupSessionOutputBody, - raw=raw, - ) - - def get_test_session(self, id: str, raw: bool = False, **query: Any) -> Any: - return self._request( - "GET", - "/setup/test-sessions/{id}", - params={**query, "id": id}, - body=None, - response_model=SetupSessionOutputBody, - raw=raw, - ) - - def get_test_session_result(self, id: str, raw: bool = False, **query: Any) -> Any: - return self._request( - "GET", - "/setup/test-sessions/{id}/result", - params={**query, "id": id}, - body=None, - response_model=SetupSessionOutputBody, - raw=raw, - ) - - def retry_test_session(self, id: str, raw: bool = False, **query: Any) -> Any: - return self._request( - "POST", - "/setup/test-sessions/{id}/retry", - params={**query, "id": id}, - body=None, - response_model=SetupSessionOutputBody, - raw=raw, - ) - - def submit_test_session_events( - self, - id: str, - body: SetupSubmitSessionInputBody, - integrity_test_session: str | None = None, - raw: bool = False, - **query: Any, - ) -> Any: - return self._request( - "POST", - "/setup/test-sessions/{id}/submit", - params={**query, "id": id}, - body=body, - response_model=BatchResponse, - raw=raw, - headers={"X-Integrity-Test-Session": integrity_test_session} - if integrity_test_session - else None, - ) - - -class AsyncSetup(AsyncResource): - async def list_setup_integrations(self, raw: bool = False, **query: Any) -> Any: - return await self._request( - "GET", - "/setup/integrations", - params=query, - body=None, - response_model=SetupIntegrationsOutputBody, - raw=raw, - ) - - async def get_setup_integration(self, id: str, raw: bool = False, **query: Any) -> Any: - return await self._request( - "GET", - "/setup/integrations/{id}", - params={**query, "id": id}, - body=None, - response_model=SetupIntegrationOutputBody, - raw=raw, - ) - - async def list_setup_sources(self, raw: bool = False, **query: Any) -> Any: - return await self._request( - "GET", - "/setup/sources", - params=query, - body=None, - response_model=SetupSourcesOutputBody, - raw=raw, - ) - - async def create_setup_source( - self, body: CreateSourceRequest, raw: bool = False, **query: Any - ) -> Any: - return await self._request( - "POST", - "/setup/sources", - params=query, - body=body, - response_model=SetupSourceOutputBody, - raw=raw, - ) - - async def get_setup_source(self, id: str, raw: bool = False, **query: Any) -> Any: - return await self._request( - "GET", - "/setup/sources/{id}", - params={**query, "id": id}, - body=None, - response_model=SetupSourceOutputBody, - raw=raw, - ) - - async def update_setup_source( - self, id: str, body: UpdateSourceRequest, raw: bool = False, **query: Any - ) -> Any: - return await self._request( - "PATCH", - "/setup/sources/{id}", - params={**query, "id": id}, - body=body, - response_model=SetupSourceOutputBody, - raw=raw, - ) - - async def disable_setup_source(self, id: str, raw: bool = False, **query: Any) -> Any: - return await self._request( - "DELETE", - "/setup/sources/{id}", - params={**query, "id": id}, - body=None, - response_model=None, - raw=raw, - ) - - async def create_test_session( - self, body: CreateSessionRequest, raw: bool = False, **query: Any - ) -> Any: - return await self._request( - "POST", - "/setup/test-sessions", - params=query, - body=body, - response_model=SetupSessionOutputBody, - raw=raw, - ) - - async def get_test_session(self, id: str, raw: bool = False, **query: Any) -> Any: - return await self._request( - "GET", - "/setup/test-sessions/{id}", - params={**query, "id": id}, - body=None, - response_model=SetupSessionOutputBody, - raw=raw, - ) - - async def get_test_session_result(self, id: str, raw: bool = False, **query: Any) -> Any: - return await self._request( - "GET", - "/setup/test-sessions/{id}/result", - params={**query, "id": id}, - body=None, - response_model=SetupSessionOutputBody, - raw=raw, - ) - - async def retry_test_session(self, id: str, raw: bool = False, **query: Any) -> Any: - return await self._request( - "POST", - "/setup/test-sessions/{id}/retry", - params={**query, "id": id}, - body=None, - response_model=SetupSessionOutputBody, - raw=raw, - ) - - async def submit_test_session_events( - self, - id: str, - body: SetupSubmitSessionInputBody, - integrity_test_session: str | None = None, - raw: bool = False, - **query: Any, - ) -> Any: - return await self._request( - "POST", - "/setup/test-sessions/{id}/submit", - params={**query, "id": id}, - body=body, - response_model=BatchResponse, - raw=raw, - headers={"X-Integrity-Test-Session": integrity_test_session} - if integrity_test_session - else None, - ) - - -OPERATION_SPECS = { - "list_setup_integrations": ("setup", "GET", "/setup/integrations"), - "get_setup_integration": ("setup", "GET", "/setup/integrations/{id}"), - "list_setup_sources": ("setup", "GET", "/setup/sources"), - "create_setup_source": ("setup", "POST", "/setup/sources"), - "get_setup_source": ("setup", "GET", "/setup/sources/{id}"), - "update_setup_source": ("setup", "PATCH", "/setup/sources/{id}"), - "disable_setup_source": ("setup", "DELETE", "/setup/sources/{id}"), - "create_test_session": ("setup", "POST", "/setup/test-sessions"), - "get_test_session": ("setup", "GET", "/setup/test-sessions/{id}"), - "get_test_session_result": ("setup", "GET", "/setup/test-sessions/{id}/result"), - "retry_test_session": ("setup", "POST", "/setup/test-sessions/{id}/retry"), - "submit_test_session_events": ("setup", "POST", "/setup/test-sessions/{id}/submit"), -} diff --git a/src/reconify/resources/transactions.py b/src/reconify/resources/transactions.py deleted file mode 100644 index ec9bc9a..0000000 --- a/src/reconify/resources/transactions.py +++ /dev/null @@ -1,58 +0,0 @@ -"""Transactions API resource client.""" - -from __future__ import annotations - -from typing import Any - -from ..models import TransactionOutputBody, TransactionPage -from .base import AsyncResource, SyncResource - - -class Transactions(SyncResource): - def list_wallet_transactions(self, raw: bool = False, **query: Any) -> Any: - return self._request( - "GET", - "/transactions", - params=query, - body=None, - response_model=TransactionPage, - raw=raw, - ) - - def get_wallet_transaction(self, id: str, raw: bool = False, **query: Any) -> Any: - return self._request( - "GET", - "/transactions/{id}", - params={**query, "id": id}, - body=None, - response_model=TransactionOutputBody, - raw=raw, - ) - - -class AsyncTransactions(AsyncResource): - async def list_wallet_transactions(self, raw: bool = False, **query: Any) -> Any: - return await self._request( - "GET", - "/transactions", - params=query, - body=None, - response_model=TransactionPage, - raw=raw, - ) - - async def get_wallet_transaction(self, id: str, raw: bool = False, **query: Any) -> Any: - return await self._request( - "GET", - "/transactions/{id}", - params={**query, "id": id}, - body=None, - response_model=TransactionOutputBody, - raw=raw, - ) - - -OPERATION_SPECS = { - "list_wallet_transactions": ("transactions", "GET", "/transactions"), - "get_wallet_transaction": ("transactions", "GET", "/transactions/{id}"), -} diff --git a/src/reconify/resources/wallets.py b/src/reconify/resources/wallets.py deleted file mode 100644 index a9fb523..0000000 --- a/src/reconify/resources/wallets.py +++ /dev/null @@ -1,79 +0,0 @@ -"""Wallets API resource client.""" - -from __future__ import annotations - -from typing import Any - -from ..models import WalletBalanceOutputBody, WalletOutputBody, WalletPage -from .base import AsyncResource, SyncResource - - -class Wallets(SyncResource): - def list_wallets(self, raw: bool = False, **query: Any) -> Any: - return self._request( - "GET", - "/wallets", - params=query, - body=None, - response_model=WalletPage, - raw=raw, - ) - - def get_wallet(self, id: str, raw: bool = False, **query: Any) -> Any: - return self._request( - "GET", - "/wallets/{id}", - params={**query, "id": id}, - body=None, - response_model=WalletOutputBody, - raw=raw, - ) - - def get_wallet_balance(self, id: str, raw: bool = False, **query: Any) -> Any: - return self._request( - "GET", - "/wallets/{id}/balance", - params={**query, "id": id}, - body=None, - response_model=WalletBalanceOutputBody, - raw=raw, - ) - - -class AsyncWallets(AsyncResource): - async def list_wallets(self, raw: bool = False, **query: Any) -> Any: - return await self._request( - "GET", - "/wallets", - params=query, - body=None, - response_model=WalletPage, - raw=raw, - ) - - async def get_wallet(self, id: str, raw: bool = False, **query: Any) -> Any: - return await self._request( - "GET", - "/wallets/{id}", - params={**query, "id": id}, - body=None, - response_model=WalletOutputBody, - raw=raw, - ) - - async def get_wallet_balance(self, id: str, raw: bool = False, **query: Any) -> Any: - return await self._request( - "GET", - "/wallets/{id}/balance", - params={**query, "id": id}, - body=None, - response_model=WalletBalanceOutputBody, - raw=raw, - ) - - -OPERATION_SPECS = { - "list_wallets": ("wallets", "GET", "/wallets"), - "get_wallet": ("wallets", "GET", "/wallets/{id}"), - "get_wallet_balance": ("wallets", "GET", "/wallets/{id}/balance"), -} diff --git a/src/reconify/transport.py b/src/reconify/transport.py index a2a0ed0..4e137ff 100644 --- a/src/reconify/transport.py +++ b/src/reconify/transport.py @@ -91,12 +91,12 @@ def _validate_request_body(body: Any) -> Any: return body -def _validate_integrity_payload(path: str, body: Any) -> None: - if path not in {"/integrity/events", "/integrity/test-events"} or body is None: +def _validate_ingestion_payload(path: str, body: Any) -> None: + if path != "/events" or body is None: return payload_size = len(json.dumps(body, separators=(",", ":"), default=str).encode("utf-8")) if payload_size > 5 * 1024 * 1024: - raise ReconifyValidationError("Integrity event requests must not exceed 5 MiB") + raise ReconifyValidationError("Monitoring event requests must not exceed 5 MiB") class SyncTransport: @@ -136,7 +136,7 @@ def request( request_headers["X-Request-ID"] = self.request_id request_headers.update(headers or {}) data = _validate_request_body(body) - _validate_integrity_payload(path, data) + _validate_ingestion_payload(path, data) params = {key: value for key, value in (query or {}).items() if value is not None} attempts = 0 while True: @@ -236,7 +236,7 @@ async def request( request_headers["X-Request-ID"] = self.request_id request_headers.update(headers or {}) data = _validate_request_body(body) - _validate_integrity_payload(path, data) + _validate_ingestion_payload(path, data) params = {key: value for key, value in (query or {}).items() if value is not None} attempts = 0 while True: diff --git a/tests/test_client.py b/tests/test_client.py index 27e5478..fb90f87 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -6,73 +6,126 @@ import pytest from reconify import Reconify -from reconify.errors import ReconifyRateLimitError -from reconify.models import AlertRuleRequest, IngestEventsInputBody, IngestRow, SourceOutputBody +from reconify.errors import ( + ReconifyAuthenticationError, + ReconifyConflictError, + ReconifyNotFoundError, + ReconifyPermissionError, + ReconifyRateLimitError, + ReconifyRequestError, + ReconifyServiceUnavailableError, +) +from reconify.models import ( + AddNoteRequest, + MonitoringBatchRequest, + MonitoringEvent, + PatchIssueRequest, +) from reconify.transport import RetryConfig +def _event(event_id: str = "evt_1") -> dict[str, object]: + return { + "id": event_id, + "flow": "payment_to_wallet", + "event_type": "payment.succeeded", + "reference": "order-1", + "entity_type": "wallet", + "entity_id": "wallet-1", + "occurred_at": "2026-01-01T00:00:00Z", + "received_at": "2026-01-01T00:00:01Z", + "status": "processed", + } + + def test_base_url_and_request_contract() -> None: requests: list[httpx.Request] = [] def handler(request: httpx.Request) -> httpx.Response: requests.append(request) return httpx.Response( - 200, - json={ - "source": { - "id": "source-1", - "orgId": "org-1", - "name": "Books", - "schemaMapping": {}, - "createdAt": "2026-01-01T00:00:00Z", - "updatedAt": "2026-01-01T00:00:00Z", - } - }, - headers={"X-Request-ID": "response-id"}, - request=request, + 200, json=_event(), headers={"X-Request-ID": "response-id"}, request=request ) - transport = httpx.MockTransport(handler) - http_client = httpx.Client(transport=transport) with Reconify( "rk_test", base_url="http://localhost:3002/v1/", request_id="caller-id", - http_client=http_client, + http_client=httpx.Client(transport=httpx.MockTransport(handler)), ) as client: - result = client.ledger.get_ledger_source("source id") + result = client.events.get_event("event id") - assert isinstance(result, SourceOutputBody) - assert requests[0].method == "GET" - assert str(requests[0].url) == "http://localhost:3002/v1/ledger/sources/source%20id" + assert result.id == "evt_1" + assert str(requests[0].url) == "http://localhost:3002/v1/events/event%20id" assert requests[0].headers["Authorization"] == "Bearer rk_test" assert requests[0].headers["X-Request-ID"] == "caller-id" -def test_base_url_can_come_from_environment(monkeypatch: pytest.MonkeyPatch) -> None: +def test_base_url_and_key_can_come_from_environment(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("RECONIFY_API_URL", "http://api.test/v1/") + monkeypatch.setenv("RECONIFY_API_KEY", "rk_environment") requests: list[httpx.Request] = [] def handler(request: httpx.Request) -> httpx.Response: requests.append(request) - return httpx.Response(200, json={"events": [], "limit": 1}, request=request) + return httpx.Response(200, json={"status": "operational"}, request=request) + + with Reconify(http_client=httpx.Client(transport=httpx.MockTransport(handler))) as client: + client.metadata.get_health() + + assert str(requests[0].url) == "http://api.test/v1/health" + assert requests[0].headers["Authorization"] == "Bearer rk_environment" + + +def test_ingestion_serializes_typed_body() -> None: + requests: list[httpx.Request] = [] + def handler(request: httpx.Request) -> httpx.Response: + requests.append(request) + return httpx.Response(202, json={"results": []}, request=request) + + body = MonitoringBatchRequest( + events=[ + MonitoringEvent( + flow="payment_to_wallet", + type="payment.succeeded", + reference="order-1", + entity_id="wallet-1", + amount="10.00", + currency="USD", + ) + ] + ) with Reconify( "rk_test", + base_url="http://api.test", http_client=httpx.Client(transport=httpx.MockTransport(handler)), ) as client: - client.events.list_events() + client.ingestion.ingest_monitoring_events(body) - assert str(requests[0].url) == "http://api.test/v1/events" + assert json.loads(requests[0].content)["events"][0]["entity_id"] == "wallet-1" + assert requests[0].url.path == "/v1/events" -def test_json_aliases_and_raw_response() -> None: +def test_note_idempotency_header_and_issue_assignment() -> None: requests: list[httpx.Request] = [] def handler(request: httpx.Request) -> httpx.Response: requests.append(request) + if request.method == "POST": + return httpx.Response(201, json={"id": "note-1", "body": "checked"}, request=request) return httpx.Response( - 200, content=b'{"status":"ok"}', headers={"X-Request-ID": "r"}, request=request + 200, + json={ + "id": "issue-1", + "status": "open", + "category": "missing_event", + "severity": "medium", + "message": "missing evidence", + "assigned_to": None, + "opened_at": "2026-01-01T00:00:00Z", + }, + request=request, ) with Reconify( @@ -80,43 +133,20 @@ def handler(request: httpx.Request) -> httpx.Response: base_url="http://api.test", http_client=httpx.Client(transport=httpx.MockTransport(handler)), ) as client: - response = client.alerts.put_alert_rule( - AlertRuleRequest( - breachEnabled=True, - controlId="control-1", - dedupWindowSeconds=60, - destinations={}, - resolutionEnabled=True, - severityMin="low", - ), - raw=True, + client.issues.add_issue_note( + "issue-1", AddNoteRequest(body="checked"), idempotency_key="note-1" ) + client.issues.update_issue("issue-1", PatchIssueRequest(assigned_to=None)) - assert response.status_code == 200 - assert response.request_id == "r" - assert json.loads(response.body) == {"status": "ok"} - assert requests[0].headers["Content-Type"] == "application/json" - - -def test_204_returns_none() -> None: - with Reconify( - "rk_test", - base_url="http://api.test", - http_client=httpx.Client( - transport=httpx.MockTransport(lambda request: httpx.Response(204, request=request)) - ), - ) as client: - assert client.ledger.delete_ledger_source("source-1") is None + assert requests[0].headers["Idempotency-Key"] == "note-1" + assert requests[1].method == "PATCH" def test_errors_are_typed_and_safe() -> None: def handler(request: httpx.Request) -> httpx.Response: return httpx.Response( 429, - json={ - "title": "Too Many Requests", - "errors": [{"location": "$code", "message": "rate_limited"}], - }, + json={"title": "Too Many Requests", "code": "rate_limited", "message": "slow down"}, headers={"X-Request-ID": "error-id"}, request=request, ) @@ -135,70 +165,54 @@ def handler(request: httpx.Request) -> httpx.Response: assert "rk_secret" not in str(caught.value) -def test_after_takes_precedence_over_offset() -> None: - requests: list[httpx.Request] = [] - +@pytest.mark.parametrize( + ("status", "error_type"), + [ + (400, ReconifyRequestError), + (401, ReconifyAuthenticationError), + (403, ReconifyPermissionError), + (404, ReconifyNotFoundError), + (409, ReconifyConflictError), + (429, ReconifyRateLimitError), + (503, ReconifyServiceUnavailableError), + ], +) +def test_public_error_statuses_are_typed( + status: int, error_type: type[Exception] +) -> None: def handler(request: httpx.Request) -> httpx.Response: - requests.append(request) - return httpx.Response(200, json={"events": [], "limit": 10}, request=request) + return httpx.Response( + status, + json={"code": "test_error", "message": "request rejected"}, + headers={"X-Request-ID": "status-id"}, + request=request, + ) with Reconify( "rk_test", base_url="http://api.test", + retry=RetryConfig(max_retries=0), http_client=httpx.Client(transport=httpx.MockTransport(handler)), ) as client: - client.events.list_events(after="opaque", offset=100) + with pytest.raises(error_type) as caught: + client.events.list_events() - assert requests[0].url.params == httpx.QueryParams("after=opaque") + assert caught.value.status_code == status + assert caught.value.request_id == "status-id" -def test_timeout_is_request_scoped_and_transport_errors_retry() -> None: - attempts = 0 +def test_after_takes_precedence_over_offset() -> None: requests: list[httpx.Request] = [] def handler(request: httpx.Request) -> httpx.Response: - nonlocal attempts - attempts += 1 requests.append(request) - if attempts == 1: - raise httpx.ReadTimeout("timed out", request=request) - return httpx.Response(200, json={"events": [], "limit": 1}, request=request) + return httpx.Response(200, json={"events": [], "limit": 10}, request=request) with Reconify( "rk_test", base_url="http://api.test", - retry=RetryConfig(max_retries=1, base_delay=0, jitter=0), http_client=httpx.Client(transport=httpx.MockTransport(handler)), ) as client: - client.events.list_events(timeout=0.1) - - assert attempts == 2 - assert requests[0].url.params == httpx.QueryParams() - + client.events.list_events(after="opaque", offset=100) -def test_models_preserve_aliases_and_tolerate_new_enum_values() -> None: - row = IngestRow( - idempotencyKey="row-1", - date="2026-01-01", - amountMinor=100, - currency="USD", - direction="future_direction", - ) - assert row.idempotency_key == "row-1" - assert row.direction.value == "future_direction" - assert row.model_dump(by_alias=True)["idempotencyKey"] == "row-1" - - -def test_documented_batch_limits_are_validated() -> None: - with pytest.raises(ValueError): - IngestEventsInputBody(events=[]) - - with pytest.raises(ValueError): - IngestRow( - idempotencyKey="row-1", - date="2026-01-01", - amountMinor=100, - currency="USD", - direction="debit", - unexpected=True, - ) + assert requests[0].url.params == httpx.QueryParams("after=opaque") diff --git a/tests/test_openapi_coverage.py b/tests/test_openapi_coverage.py index 2f1c47b..a2c97f4 100644 --- a/tests/test_openapi_coverage.py +++ b/tests/test_openapi_coverage.py @@ -8,81 +8,59 @@ from reconify.resources import OPERATION_SPECS, SYNC_RESOURCE_CLASSES -EXCLUDED_PATHS = { - "/reconciliations/{id}/adjustments", - "/reconciliations/{id}/adjustments/{adjustment_id}", - "/reconciliations/{id}/close", - "/reconciliations/{id}/reopen", - "/reconciliations/{id}/evidence", - "/reconciliations/{id}/evidence/{evidence_id}", - "/reconciliations/{id}/reports/reconciliation/items", - "/reconciliations/{id}/signoffs", - "/reconciliations/{id}/signoffs/{role}", -} - def _openapi_path() -> Path | None: candidates = [] - if os.getenv("RECONIFY_OPENAPI_PATH"): - candidates.append(Path(os.environ["RECONIFY_OPENAPI_PATH"])) - candidates.append( - Path( - "/Users/koladev/conductor/workspaces/reconify-saas/pattaya/openapi/reconify.openapi.json" - ) - ) + if os.getenv("RECONIFY_OPENAPI_SPEC"): + candidates.append(Path(os.environ["RECONIFY_OPENAPI_SPEC"])) + candidates.append(Path(__file__).parents[1] / ".contract" / "reconify.openapi.json") return next((candidate for candidate in candidates if candidate.is_file()), None) -def _operations() -> list[tuple[str, str]]: +def _operations() -> list[tuple[str, str, str]]: path = _openapi_path() if path is None: - pytest.skip("OpenAPI source is not available; set RECONIFY_OPENAPI_PATH") + pytest.skip("OpenAPI source is not available; run scripts/fetch_contract.py") document = json.loads(path.read_text()) return [ - (operation["operationId"], route) + (operation["operationId"], method.upper(), route.removeprefix("/v1") or "/") for route, methods in document["paths"].items() for method, operation in methods.items() if method.lower() in {"get", "post", "put", "patch", "delete"} - and route not in EXCLUDED_PATHS ] -def test_openapi_requires_exactly_fifty_retained_operations() -> None: - operations = _operations() - assert len(operations) == 50 - assert len(OPERATION_SPECS) == 50 +def test_openapi_contains_only_public_routes() -> None: + path = _openapi_path() + if path is None: + pytest.skip("OpenAPI source is not available; run scripts/fetch_contract.py") + document = json.loads(path.read_text()) + assert all(not route.startswith("/business/") for route in document["paths"]) -def test_every_retained_openapi_operation_has_a_public_method() -> None: +def test_every_openapi_operation_has_a_public_method() -> None: operations = _operations() - for operation_id, route in operations: + assert len(operations) == 13 + assert len({operation_id for operation_id, _, _ in operations}) == len(operations) + assert len(OPERATION_SPECS) == len(operations) + for operation_id, verb, route in operations: method_name = operation_id.replace("-", "_") assert method_name in OPERATION_SPECS, f"Missing SDK contract for {operation_id}" - group, verb, registered_route = OPERATION_SPECS[method_name] + group, registered_verb, registered_route = OPERATION_SPECS[method_name] assert registered_route == route - assert verb in {"GET", "POST", "PUT", "PATCH", "DELETE"} + assert registered_verb == verb assert hasattr(SYNC_RESOURCE_CLASSES[group], method_name), ( f"Missing SDK method for {operation_id}" ) -def test_excluded_reconciliation_operations_are_not_public() -> None: - excluded_ids = { - "list_reconciliation_adjustments", - "create_reconciliation_adjustment", - "get_reconciliation_adjustment", - "close_reconciliation", - "reopen_reconciliation", - "list_reconciliation_evidence", - "create_reconciliation_evidence", - "get_reconciliation_evidence", - "list_reconciliation_items", - "list_reconciliation_signoffs", - "upsert_reconciliation_signoff", - "delete_reconciliation_signoff", +def test_legacy_operations_are_not_public() -> None: + legacy_names = { + "list_ledger_sources", + "list_wallets", + "list_setup_sources", + "search_integrity_resources", + "list_alert_rules", + "list_reconciliations", } - assert not any( - hasattr(SYNC_RESOURCE_CLASSES["reconciliations"], method_name) - for method_name in excluded_ids - ) - assert not excluded_ids.intersection(OPERATION_SPECS) + assert not legacy_names.intersection(OPERATION_SPECS) diff --git a/tests/test_pagination.py b/tests/test_pagination.py index fd0bffd..004443a 100644 --- a/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -7,18 +7,15 @@ def _event(event_id: str) -> dict[str, object]: return { - "amountMinor": 100, - "applied": True, - "canonicalHash": f"hash-{event_id}", - "currency": "USD", - "eventSchemaVersion": 1, - "eventType": "payment", "id": event_id, - "occurredAt": "2026-01-01T00:00:00Z", - "processingStatus": "applied", - "receivedAt": "2026-01-01T00:00:01Z", - "sourceEventId": f"source-{event_id}", - "sourceId": "source-1", + "flow": "payment_to_wallet", + "event_type": "payment.succeeded", + "reference": event_id, + "entity_type": "wallet", + "entity_id": "wallet-1", + "occurred_at": "2026-01-01T00:00:00Z", + "received_at": "2026-01-01T00:00:01Z", + "status": "processed", } @@ -31,12 +28,12 @@ def handler(request: httpx.Request) -> httpx.Response: if "after" not in params: return httpx.Response( 200, - json={"events": [_event("event-1")], "limit": 1, "nextCursor": "cursor-1"}, + json={"events": [_event("event-1")], "limit": 1, "next_cursor": "cursor-1"}, request=request, ) return httpx.Response( 200, - json={"events": [_event("event-2")], "limit": 1, "nextCursor": None}, + json={"events": [_event("event-2")], "limit": 1}, request=request, ) @@ -60,13 +57,11 @@ def handler(request: httpx.Request) -> httpx.Response: if "after" not in params: return httpx.Response( 200, - json={"events": [_event("event-1")], "limit": 1, "nextCursor": "cursor-1"}, + json={"events": [_event("event-1")], "limit": 1, "next_cursor": "cursor-1"}, request=request, ) return httpx.Response( - 200, - json={"events": [_event("event-2")], "limit": 1, "nextCursor": None}, - request=request, + 200, json={"events": [_event("event-2")], "limit": 1}, request=request ) async with AsyncReconify( diff --git a/uv.lock b/uv.lock index cf8c761..f976109 100644 --- a/uv.lock +++ b/uv.lock @@ -567,7 +567,7 @@ wheels = [ [[package]] name = "reconify-python" -version = "0.1.0" +version = "1.0.0" source = { editable = "." } dependencies = [ { name = "httpx" },