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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .openapi-contract.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"version": "1.0.0",
"sha256": "04170b7eda8a7bacef6591a7132b9a5cbdb62963af9ca6260e1948c1ee8233c1"
"version": "2.0.0",
"sha256": "5cccdf920a62fb325c7fcf9410728889858d156831a78760e41e3f98774db7f2"
}
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down
12 changes: 6 additions & 6 deletions UPGRADING.md
Original file line number Diff line number Diff line change
@@ -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:

Expand All @@ -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.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions src/reconify/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
43 changes: 30 additions & 13 deletions src/reconify/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__ = [
Expand All @@ -50,5 +66,6 @@
"Organization",
"ASYNC_RESOURCE_CLASSES",
"OPERATION_SPECS",
"OPERATION_METHODS",
"SYNC_RESOURCE_CLASSES",
]
10 changes: 5 additions & 5 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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] = []

Expand All @@ -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"


Expand Down Expand Up @@ -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:
Expand Down
10 changes: 5 additions & 5 deletions tests/test_openapi_coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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"}
Expand All @@ -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), (
Expand Down
Loading