From 1e676a3d5c796fef06f57023b1fcba19d5efe8a4 Mon Sep 17 00:00:00 2001 From: Mangabo Kolawole Date: Sat, 22 Aug 2026 22:10:09 +0100 Subject: [PATCH] release: publish SDK v2.0.0 --- .openapi-contract.json | 4 +-- README.md | 14 +++++----- UPGRADING.md | 12 ++++----- pyproject.toml | 2 +- src/reconify/client.py | 4 +-- src/reconify/resources/__init__.py | 43 +++++++++++++++++++++--------- tests/test_client.py | 10 +++---- tests/test_openapi_coverage.py | 10 +++---- 8 files changed, 58 insertions(+), 41 deletions(-) diff --git a/.openapi-contract.json b/.openapi-contract.json index c59262e..29ae9b7 100644 --- a/.openapi-contract.json +++ b/.openapi-contract.json @@ -1,4 +1,4 @@ { - "version": "1.0.0", - "sha256": "04170b7eda8a7bacef6591a7132b9a5cbdb62963af9ca6260e1948c1ee8233c1" + "version": "2.0.0", + "sha256": "5cccdf920a62fb325c7fcf9410728889858d156831a78760e41e3f98774db7f2" } diff --git a/README.md b/README.md index a185d9d..1ca97e0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Reconify Python SDK -Typed synchronous and asynchronous clients for the public Reconify v1 API. +Typed synchronous and asynchronous clients for the public Reconify v2 API. ## Installation and quickstart @@ -18,8 +18,8 @@ with Reconify(api_key="rk_...") as client: ``` 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, and URLs with or without `/v1` are accepted. +`https://api.reconifyhq.com/v2`. `RECONIFY_API_URL` or `base_url` can select a +staging or self-hosted endpoint, and URLs with or without `/v2` are accepted. ## Public resources @@ -56,11 +56,11 @@ The default source is the public manifest at set `RECONIFY_OPENAPI_SPEC` to an explicit OpenAPI JSON file. The SDK never depends on another checkout or an absolute workspace path. -## Migration to 1.0.0 +## Migration to 2.0.0 -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 +Version `2.0.0` targets the v2 monitoring and issue-investigation API at `/v2`. +Generated operation IDs use stable `resource_action` identifiers while the +Python resource methods retain their snake_case names. See [UPGRADING.md](UPGRADING.md). ## Build and release diff --git a/UPGRADING.md b/UPGRADING.md index 26c2339..4ea7ed0 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,8 +1,8 @@ -# Upgrading to 1.0.0 +# Upgrading to 2.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. +The 2.0.0 client targets the public Reconify v2 contract at `/v2`. +Generated operation IDs now use stable `resource_action` identifiers. The +Python resource methods keep their existing snake_case names. Use these resources: @@ -13,5 +13,5 @@ Use these resources: - 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. +tolerant string enums. Existing v1 clients can continue using `/v1`; v2 +clients must use the v2 artifact and endpoint. diff --git a/pyproject.toml b/pyproject.toml index 0c0e2a8..efb8eab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "reconify-python" -version = "1.0.0" +version = "2.0.0" description = "Typed Python client for the Reconify Public API" readme = "README.md" requires-python = ">=3.10" diff --git a/src/reconify/client.py b/src/reconify/client.py index 4683c86..76e07cf 100644 --- a/src/reconify/client.py +++ b/src/reconify/client.py @@ -26,12 +26,12 @@ ) from .transport import AsyncTransport, RetryConfig, SyncTransport -DEFAULT_BASE_URL = "https://api.reconifyhq.com/v1" +DEFAULT_BASE_URL = "https://api.reconifyhq.com/v2" def _normalize_base_url(base_url: str | None) -> str: value = (base_url or os.getenv("RECONIFY_API_URL") or DEFAULT_BASE_URL).rstrip("/") - return value if value.endswith("/v1") else f"{value}/v1" + return value if value.endswith("/v2") else f"{value}/v2" def _api_key(api_key: str | None) -> str: diff --git a/src/reconify/resources/__init__.py b/src/reconify/resources/__init__.py index 7d8a841..1d9465f 100644 --- a/src/reconify/resources/__init__.py +++ b/src/reconify/resources/__init__.py @@ -22,19 +22,35 @@ } OPERATION_SPECS = { - "get_api_info": ("metadata", "GET", "/"), - "list_events": ("events", "GET", "/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": ("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"), + "api_info_get": ("metadata", "GET", "/"), + "events_list": ("events", "GET", "/events"), + "events_ingest": ("ingestion", "POST", "/events"), + "events_get": ("events", "GET", "/events/{event_id}"), + "health_get": ("metadata", "GET", "/health"), + "issues_list": ("issues", "GET", "/issues"), + "issues_get": ("issues", "GET", "/issues/{issue_id}"), + "issues_assign": ("issues", "PATCH", "/issues/{issue_id}"), + "issues_list_events": ("events", "GET", "/issues/{issue_id}/events"), + "issues_list_notes": ("issues", "GET", "/issues/{issue_id}/notes"), + "issues_add_note": ("issues", "POST", "/issues/{issue_id}/notes"), + "organization_get": ("organization", "GET", "/organization"), + "organization_list_members": ("organization", "GET", "/organization/members"), +} + +OPERATION_METHODS = { + "api_info_get": "get_api_info", + "events_list": "list_events", + "events_ingest": "ingest_monitoring_events", + "events_get": "get_event", + "health_get": "get_health", + "issues_list": "list_issues", + "issues_get": "get_issue", + "issues_assign": "update_issue", + "issues_list_events": "list_issue_events", + "issues_list_notes": "list_issue_notes", + "issues_add_note": "add_issue_note", + "organization_get": "get_organization", + "organization_list_members": "list_organization_members", } __all__ = [ @@ -50,5 +66,6 @@ "Organization", "ASYNC_RESOURCE_CLASSES", "OPERATION_SPECS", + "OPERATION_METHODS", "SYNC_RESOURCE_CLASSES", ] diff --git a/tests/test_client.py b/tests/test_client.py index fb90f87..5c0e24c 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -49,20 +49,20 @@ def handler(request: httpx.Request) -> httpx.Response: with Reconify( "rk_test", - base_url="http://localhost:3002/v1/", + base_url="http://localhost:3002/v2/", request_id="caller-id", http_client=httpx.Client(transport=httpx.MockTransport(handler)), ) as client: result = client.events.get_event("event id") assert result.id == "evt_1" - assert str(requests[0].url) == "http://localhost:3002/v1/events/event%20id" + assert str(requests[0].url) == "http://localhost:3002/v2/events/event%20id" assert requests[0].headers["Authorization"] == "Bearer rk_test" assert requests[0].headers["X-Request-ID"] == "caller-id" 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_URL", "http://api.test/v2/") monkeypatch.setenv("RECONIFY_API_KEY", "rk_environment") requests: list[httpx.Request] = [] @@ -73,7 +73,7 @@ def handler(request: httpx.Request) -> httpx.Response: 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 str(requests[0].url) == "http://api.test/v2/health" assert requests[0].headers["Authorization"] == "Bearer rk_environment" @@ -104,7 +104,7 @@ def handler(request: httpx.Request) -> httpx.Response: client.ingestion.ingest_monitoring_events(body) assert json.loads(requests[0].content)["events"][0]["entity_id"] == "wallet-1" - assert requests[0].url.path == "/v1/events" + assert requests[0].url.path == "/v2/events" def test_note_idempotency_header_and_issue_assignment() -> None: diff --git a/tests/test_openapi_coverage.py b/tests/test_openapi_coverage.py index a2c97f4..70a00fd 100644 --- a/tests/test_openapi_coverage.py +++ b/tests/test_openapi_coverage.py @@ -6,7 +6,7 @@ import pytest -from reconify.resources import OPERATION_SPECS, SYNC_RESOURCE_CLASSES +from reconify.resources import OPERATION_METHODS, OPERATION_SPECS, SYNC_RESOURCE_CLASSES def _openapi_path() -> Path | None: @@ -23,7 +23,7 @@ def _operations() -> list[tuple[str, str, str]]: pytest.skip("OpenAPI source is not available; run scripts/fetch_contract.py") document = json.loads(path.read_text()) return [ - (operation["operationId"], method.upper(), route.removeprefix("/v1") or "/") + (operation["operationId"], method.upper(), route.removeprefix("/v2") or "/") for route, methods in document["paths"].items() for method, operation in methods.items() if method.lower() in {"get", "post", "put", "patch", "delete"} @@ -44,9 +44,9 @@ def test_every_openapi_operation_has_a_public_method() -> None: 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, registered_verb, registered_route = OPERATION_SPECS[method_name] + method_name = OPERATION_METHODS[operation_id] + assert operation_id in OPERATION_SPECS, f"Missing SDK contract for {operation_id}" + group, registered_verb, registered_route = OPERATION_SPECS[operation_id] assert registered_route == route assert registered_verb == verb assert hasattr(SYNC_RESOURCE_CLASSES[group], method_name), (