reconify-python 2.0.0__tar.gz → 2.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. reconify_python-2.2.0/.openapi-contract.json +4 -0
  2. {reconify_python-2.0.0 → reconify_python-2.2.0}/CHANGELOG.md +13 -0
  3. {reconify_python-2.0.0 → reconify_python-2.2.0}/PKG-INFO +7 -3
  4. {reconify_python-2.0.0 → reconify_python-2.2.0}/README.md +6 -2
  5. reconify_python-2.2.0/UPGRADING.md +61 -0
  6. {reconify_python-2.0.0 → reconify_python-2.2.0}/pyproject.toml +1 -1
  7. {reconify_python-2.0.0 → reconify_python-2.2.0}/src/reconify/__init__.py +1 -1
  8. {reconify_python-2.0.0 → reconify_python-2.2.0}/src/reconify/client.py +4 -0
  9. {reconify_python-2.0.0 → reconify_python-2.2.0}/src/reconify/models.py +111 -0
  10. {reconify_python-2.0.0 → reconify_python-2.2.0}/src/reconify/resources/__init__.py +7 -0
  11. {reconify_python-2.0.0 → reconify_python-2.2.0}/src/reconify/resources/ingestion.py +9 -3
  12. reconify_python-2.2.0/src/reconify/resources/onchain.py +57 -0
  13. {reconify_python-2.0.0 → reconify_python-2.2.0}/src/reconify/transport.py +2 -0
  14. {reconify_python-2.0.0 → reconify_python-2.2.0}/tests/test_client.py +116 -4
  15. {reconify_python-2.0.0 → reconify_python-2.2.0}/tests/test_openapi_coverage.py +1 -1
  16. {reconify_python-2.0.0 → reconify_python-2.2.0}/uv.lock +1 -1
  17. reconify_python-2.0.0/.openapi-contract.json +0 -4
  18. reconify_python-2.0.0/UPGRADING.md +0 -17
  19. {reconify_python-2.0.0 → reconify_python-2.2.0}/.bumpversion.cfg +0 -0
  20. {reconify_python-2.0.0 → reconify_python-2.2.0}/.conductor/settings.toml +0 -0
  21. {reconify_python-2.0.0 → reconify_python-2.2.0}/.github/workflows/ci.yml +0 -0
  22. {reconify_python-2.0.0 → reconify_python-2.2.0}/.github/workflows/release.yml +0 -0
  23. {reconify_python-2.0.0 → reconify_python-2.2.0}/.github/workflows/sync-contract.yml +0 -0
  24. {reconify_python-2.0.0 → reconify_python-2.2.0}/.gitignore +0 -0
  25. {reconify_python-2.0.0 → reconify_python-2.2.0}/AGENTS.md +0 -0
  26. {reconify_python-2.0.0 → reconify_python-2.2.0}/LICENSE +0 -0
  27. {reconify_python-2.0.0 → reconify_python-2.2.0}/context7.json +0 -0
  28. {reconify_python-2.0.0 → reconify_python-2.2.0}/reports/api-error-data-report.md +0 -0
  29. {reconify_python-2.0.0 → reconify_python-2.2.0}/scripts/fetch_contract.py +0 -0
  30. {reconify_python-2.0.0 → reconify_python-2.2.0}/src/reconify/errors.py +0 -0
  31. {reconify_python-2.0.0 → reconify_python-2.2.0}/src/reconify/pagination.py +0 -0
  32. {reconify_python-2.0.0 → reconify_python-2.2.0}/src/reconify/py.typed +0 -0
  33. {reconify_python-2.0.0 → reconify_python-2.2.0}/src/reconify/resources/base.py +0 -0
  34. {reconify_python-2.0.0 → reconify_python-2.2.0}/src/reconify/resources/events.py +0 -0
  35. {reconify_python-2.0.0 → reconify_python-2.2.0}/src/reconify/resources/issues.py +0 -0
  36. {reconify_python-2.0.0 → reconify_python-2.2.0}/src/reconify/resources/metadata.py +0 -0
  37. {reconify_python-2.0.0 → reconify_python-2.2.0}/src/reconify/resources/organization.py +0 -0
  38. {reconify_python-2.0.0 → reconify_python-2.2.0}/tests/test_pagination.py +0 -0
@@ -0,0 +1,4 @@
1
+ {
2
+ "version": "2.3.0",
3
+ "sha256": "cba4133401cc2364b4de7f270c4de43f151775be4d8930232888f2f5e70b6920"
4
+ }
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.2.0
4
+
5
+ - Synced with the public API contract 2.3.0.
6
+ - Added settlement event types, causation and revision metadata, financial
7
+ breakdowns, and settlement allocations.
8
+ - Added single-event and bare-array ingestion payloads alongside wrapped batches.
9
+ - Added synchronous and asynchronous on-chain evidence source registration.
10
+
11
+ ## 2.1.0
12
+
13
+ - Added optional `correlation_id` support to the typed monitoring event request
14
+ model and ingestion serialization.
15
+
3
16
  ## 1.0.0
4
17
 
5
18
  - Rebuilt the client for the current 13-operation public monitoring API.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: reconify-python
3
- Version: 2.0.0
3
+ Version: 2.2.0
4
4
  Summary: Typed Python client for the Reconify Public API
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -41,8 +41,9 @@ staging or self-hosted endpoint, and URLs with or without `/v2` are accepted.
41
41
 
42
42
  ## Public resources
43
43
 
44
- The client exposes metadata, events, ingestion, issues, and organization. The
45
- current public contract contains exactly 13 operations. Python methods use
44
+ The client exposes metadata, events, ingestion, issues, organization, and
45
+ on-chain evidence registration. The current public contract contains exactly
46
+ 14 operations. Python methods use
46
47
  `snake_case` names and typed Pydantic v2 models from `reconify.models`.
47
48
 
48
49
  Sync and async clients provide cursor iterators:
@@ -81,6 +82,9 @@ Generated operation IDs use stable `resource_action` identifiers while the
81
82
  Python resource methods retain their snake_case names. See
82
83
  [UPGRADING.md](UPGRADING.md).
83
84
 
85
+ Version `2.2.0` adds settlement event fields, flexible ingestion payloads, and
86
+ on-chain evidence source registration. See [UPGRADING.md](UPGRADING.md).
87
+
84
88
  ## Build and release
85
89
 
86
90
  ```sh
@@ -23,8 +23,9 @@ staging or self-hosted endpoint, and URLs with or without `/v2` are accepted.
23
23
 
24
24
  ## Public resources
25
25
 
26
- The client exposes metadata, events, ingestion, issues, and organization. The
27
- current public contract contains exactly 13 operations. Python methods use
26
+ The client exposes metadata, events, ingestion, issues, organization, and
27
+ on-chain evidence registration. The current public contract contains exactly
28
+ 14 operations. Python methods use
28
29
  `snake_case` names and typed Pydantic v2 models from `reconify.models`.
29
30
 
30
31
  Sync and async clients provide cursor iterators:
@@ -63,6 +64,9 @@ Generated operation IDs use stable `resource_action` identifiers while the
63
64
  Python resource methods retain their snake_case names. See
64
65
  [UPGRADING.md](UPGRADING.md).
65
66
 
67
+ Version `2.2.0` adds settlement event fields, flexible ingestion payloads, and
68
+ on-chain evidence source registration. See [UPGRADING.md](UPGRADING.md).
69
+
66
70
  ## Build and release
67
71
 
68
72
  ```sh
@@ -0,0 +1,61 @@
1
+ # Upgrading to 2.0.0
2
+
3
+ The 2.0.0 client targets the public Reconify v2 contract at `/v2`.
4
+ Generated operation IDs now use stable `resource_action` identifiers. The
5
+ Python resource methods keep their existing snake_case names.
6
+
7
+ Use these resources:
8
+
9
+ - metadata: API information and health
10
+ - events: event listing, lookup, and issue-linked evidence
11
+ - ingestion: monitoring event batches
12
+ - issues: issue listing, lookup, assignment, notes, and linked data
13
+ - organization: organization and member reads
14
+
15
+ Python models use snake_case fields and preserve unknown enum values through
16
+ tolerant string enums. Existing v1 clients can continue using `/v1`; v2
17
+ clients must use the v2 artifact and endpoint.
18
+
19
+ ## Upgrading to 2.1.0
20
+
21
+ The 2.1.0 client adds the optional `correlation_id` field to
22
+ `MonitoringEvent`. Use it when the same transaction spans services that report
23
+ different references:
24
+
25
+ ```python
26
+ MonitoringEvent(
27
+ flow="payment_to_wallet",
28
+ type="payment.succeeded",
29
+ reference="order-123",
30
+ entity_id="wallet-123",
31
+ correlation_id="checkout-123",
32
+ )
33
+ ```
34
+
35
+ The value is sent as event metadata and does not change operation grouping or
36
+ evaluation behavior.
37
+
38
+ ## Upgrading to 2.2.0
39
+
40
+ The 2.2.0 client follows public API contract 2.3.0. Monitoring ingestion now
41
+ accepts a single `MonitoringEvent`, a list of events, or the existing
42
+ `MonitoringBatchRequest` wrapper. Settlement flows and event types support
43
+ financial breakdowns, allocations, causation IDs, and revision IDs.
44
+
45
+ On-chain evidence enrichment can be queued for an accepted event:
46
+
47
+ ```python
48
+ client.onchain.register_onchain_source(
49
+ OnchainSourceRequest(
50
+ flow="provider_to_settlement_account",
51
+ operation_reference="settlement-123",
52
+ source_event_id="evt_01J3Y0M8VJQ5W1R3E4J4K7N8P9",
53
+ kind="transaction",
54
+ locator=OnchainSourceLocator(
55
+ network="ethereum-mainnet",
56
+ transaction_reference="0x...",
57
+ ),
58
+ ),
59
+ idempotency_key="source-settlement-123",
60
+ )
61
+ ```
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "reconify-python"
7
- version = "2.0.0"
7
+ version = "2.2.0"
8
8
  description = "Typed Python client for the Reconify Public API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,6 +1,6 @@
1
1
  """Typed clients for the Reconify Public API."""
2
2
 
3
- __version__ = "1.0.0"
3
+ __version__ = "2.2.0"
4
4
 
5
5
  from .client import AsyncReconify, Reconify
6
6
  from .errors import (
@@ -17,11 +17,13 @@ from .resources import (
17
17
  AsyncIngestion,
18
18
  AsyncIssues,
19
19
  AsyncMetadata,
20
+ AsyncOnchain,
20
21
  AsyncOrganization,
21
22
  Events,
22
23
  Ingestion,
23
24
  Issues,
24
25
  Metadata,
26
+ Onchain,
25
27
  Organization,
26
28
  )
27
29
  from .transport import AsyncTransport, RetryConfig, SyncTransport
@@ -51,6 +53,7 @@ class Reconify:
51
53
  ingestion: Ingestion
52
54
  issues: Issues
53
55
  organization: Organization
56
+ onchain: Onchain
54
57
 
55
58
  def __init__(
56
59
  self,
@@ -108,6 +111,7 @@ class AsyncReconify:
108
111
  ingestion: AsyncIngestion
109
112
  issues: AsyncIssues
110
113
  organization: AsyncOrganization
114
+ onchain: AsyncOnchain
111
115
 
112
116
  def __init__(
113
117
  self,
@@ -36,6 +36,7 @@ class Flow(TolerantStrEnum):
36
36
  PAYMENT_TO_ORDER = "payment_to_order"
37
37
  WALLET_TO_WALLET = "wallet_to_wallet"
38
38
  WALLET_TO_PAYOUT = "wallet_to_payout"
39
+ PROVIDER_TO_SETTLEMENT_ACCOUNT = "provider_to_settlement_account"
39
40
 
40
41
 
41
42
  class EventType(TolerantStrEnum):
@@ -49,6 +50,13 @@ class EventType(TolerantStrEnum):
49
50
  WALLET_CREDITED = "wallet.credited"
50
51
  WALLET_DEBITED = "wallet.debited"
51
52
  WALLET_REFUNDED = "wallet.refunded"
53
+ SETTLEMENT_ALLOCATIONS_RECORDED = "settlement.allocations_recorded"
54
+ SETTLEMENT_CREATED = "settlement.created"
55
+ SETTLEMENT_DISBURSED = "settlement.disbursed"
56
+ SETTLEMENT_FAILED = "settlement.failed"
57
+ SETTLEMENT_FINALIZED = "settlement.finalized"
58
+ SETTLEMENT_RECEIVED = "settlement.received"
59
+ SETTLEMENT_REVISED = "settlement.revised"
52
60
 
53
61
 
54
62
  class EntityType(TolerantStrEnum):
@@ -102,6 +110,44 @@ class MonitoringErrorCode(TolerantStrEnum):
102
110
  MALFORMED_REQUEST = "malformed_request"
103
111
 
104
112
 
113
+ class OnchainSourceKind(TolerantStrEnum):
114
+ PROVIDER_PAYMENT = "provider_payment"
115
+ TRANSACTION = "transaction"
116
+ ADDRESS_REFERENCE = "address_reference"
117
+
118
+
119
+ class OnchainNetwork(TolerantStrEnum):
120
+ ETHEREUM_MAINNET = "ethereum-mainnet"
121
+ ETHEREUM_SEPOLIA = "ethereum-sepolia"
122
+ SOLANA_MAINNET = "solana-mainnet"
123
+ SOLANA_DEVNET = "solana-devnet"
124
+
125
+
126
+ class OnchainSourceStatus(TolerantStrEnum):
127
+ QUEUED = "queued"
128
+
129
+
130
+ class FinancialComponentKind(TolerantStrEnum):
131
+ FEE = "fee"
132
+ COMMISSION = "commission"
133
+ TAX = "tax"
134
+ RESERVE = "reserve"
135
+ ADJUSTMENT = "adjustment"
136
+ OTHER = "other"
137
+
138
+
139
+ class FinancialComponentEffect(TolerantStrEnum):
140
+ ADD = "add"
141
+ DEDUCT = "deduct"
142
+ INCLUDED = "included"
143
+
144
+
145
+ class SettlementAllocationType(TolerantStrEnum):
146
+ PAYMENT = "payment"
147
+ REFUND = "refund"
148
+ CHARGEBACK = "chargeback"
149
+
150
+
105
151
  class APIInfo(ResponseModel):
106
152
  name: str
107
153
  version: str
@@ -113,6 +159,31 @@ class Health(ResponseModel):
113
159
  status: str
114
160
 
115
161
 
162
+ class FinancialComponent(RequestModel):
163
+ amount: str
164
+ kind: FinancialComponentKind
165
+ effect: FinancialComponentEffect
166
+ description: str | None = None
167
+ provider_type: str | None = None
168
+ reference: str | None = None
169
+ metadata: dict[str, str | int | float | bool] | None = None
170
+
171
+
172
+ class FinancialBreakdown(RequestModel):
173
+ gross: str
174
+ net: str
175
+ components: list[FinancialComponent] = Field(max_length=100)
176
+
177
+
178
+ class SettlementAllocation(RequestModel):
179
+ allocation_id: str
180
+ allocation_type: SettlementAllocationType
181
+ flow: str
182
+ reference: str
183
+ amount: str
184
+ event_id: str | None = None
185
+
186
+
116
187
  class MonitoringEventData(RequestModel):
117
188
  provider: str | None = None
118
189
  integration_ref: str | None = None
@@ -121,6 +192,8 @@ class MonitoringEventData(RequestModel):
121
192
  failure_code: str | None = None
122
193
  failure_message: str | None = None
123
194
  retryable: bool | None = None
195
+ revises_event_id: str | None = None
196
+ allocations: list[SettlementAllocation] | None = Field(default=None, max_length=500)
124
197
 
125
198
 
126
199
  class MonitoringEvent(RequestModel):
@@ -131,8 +204,11 @@ class MonitoringEvent(RequestModel):
131
204
  entity_id: str
132
205
  occurred_at: datetime | None = None
133
206
  amount: str | None = None
207
+ correlation_id: str | None = None
208
+ causation_id: str | None = None
134
209
  currency: str | None = None
135
210
  data: MonitoringEventData | None = None
211
+ financial_breakdown: FinancialBreakdown | None = None
136
212
  metadata: dict[str, str | int | float | bool] | None = None
137
213
 
138
214
 
@@ -171,8 +247,17 @@ class Event(ResponseModel):
171
247
  received_at: datetime
172
248
  amount: str | None = None
173
249
  currency: str | None = None
250
+ causation_id: str | None = None
251
+ correlation_id: str | None = None
252
+ revises_event_id: str | None = None
174
253
  provider: str | None = None
175
254
  status: ReceiptStatus
255
+ gross: str | None = None
256
+ net: str | None = None
257
+ component_count: int | None = None
258
+ allocation_count: int | None = None
259
+ financial_breakdown: FinancialBreakdown | None = None
260
+ allocations: list[SettlementAllocation] | None = None
176
261
 
177
262
 
178
263
  class ListEventsResponse(ResponseModel):
@@ -243,5 +328,31 @@ class Error(ResponseModel):
243
328
  detail: str | None = None
244
329
 
245
330
 
331
+ class OnchainSourceLocator(RequestModel):
332
+ integration_ref: str | None = None
333
+ network: OnchainNetwork | None = None
334
+ protocol_reference: str | None = None
335
+ provider_reference: str | None = None
336
+ provider_transaction_id: str | None = None
337
+ receiving_address: str | None = None
338
+ transaction_reference: str | None = None
339
+ window_end: datetime | None = None
340
+ window_start: datetime | None = None
341
+
342
+
343
+ class OnchainSourceRequest(RequestModel):
344
+ flow: str
345
+ operation_reference: str = Field(min_length=1, max_length=256)
346
+ source_event_id: str = Field(min_length=1, max_length=256)
347
+ kind: OnchainSourceKind
348
+ locator: OnchainSourceLocator
349
+
350
+
351
+ class OnchainSourceResponse(ResponseModel):
352
+ id: str
353
+ status: OnchainSourceStatus
354
+ duplicate: bool
355
+
356
+
246
357
  def model_dump(model: BaseModel) -> dict[str, Any]:
247
358
  return model.model_dump(mode="json", by_alias=True, exclude_none=True, exclude_unset=True)
@@ -4,6 +4,7 @@ from .events import AsyncEvents, Events
4
4
  from .ingestion import AsyncIngestion, Ingestion
5
5
  from .issues import AsyncIssues, Issues
6
6
  from .metadata import AsyncMetadata, Metadata
7
+ from .onchain import AsyncOnchain, Onchain
7
8
  from .organization import AsyncOrganization, Organization
8
9
 
9
10
  SYNC_RESOURCE_CLASSES = {
@@ -12,6 +13,7 @@ SYNC_RESOURCE_CLASSES = {
12
13
  "ingestion": Ingestion,
13
14
  "issues": Issues,
14
15
  "organization": Organization,
16
+ "onchain": Onchain,
15
17
  }
16
18
  ASYNC_RESOURCE_CLASSES = {
17
19
  "metadata": AsyncMetadata,
@@ -19,6 +21,7 @@ ASYNC_RESOURCE_CLASSES = {
19
21
  "ingestion": AsyncIngestion,
20
22
  "issues": AsyncIssues,
21
23
  "organization": AsyncOrganization,
24
+ "onchain": AsyncOnchain,
22
25
  }
23
26
 
24
27
  OPERATION_SPECS = {
@@ -35,6 +38,7 @@ OPERATION_SPECS = {
35
38
  "issues_add_note": ("issues", "POST", "/issues/{issue_id}/notes"),
36
39
  "organization_get": ("organization", "GET", "/organization"),
37
40
  "organization_list_members": ("organization", "GET", "/organization/members"),
41
+ "register-onchain-source": ("onchain", "POST", "/onchain-sources"),
38
42
  }
39
43
 
40
44
  OPERATION_METHODS = {
@@ -51,6 +55,7 @@ OPERATION_METHODS = {
51
55
  "issues_add_note": "add_issue_note",
52
56
  "organization_get": "get_organization",
53
57
  "organization_list_members": "list_organization_members",
58
+ "register-onchain-source": "register_onchain_source",
54
59
  }
55
60
 
56
61
  __all__ = [
@@ -59,11 +64,13 @@ __all__ = [
59
64
  "AsyncIssues",
60
65
  "AsyncMetadata",
61
66
  "AsyncOrganization",
67
+ "AsyncOnchain",
62
68
  "Events",
63
69
  "Ingestion",
64
70
  "Issues",
65
71
  "Metadata",
66
72
  "Organization",
73
+ "Onchain",
67
74
  "ASYNC_RESOURCE_CLASSES",
68
75
  "OPERATION_SPECS",
69
76
  "OPERATION_METHODS",
@@ -4,13 +4,16 @@ from __future__ import annotations
4
4
 
5
5
  from typing import Any
6
6
 
7
- from ..models import MonitoringBatchRequest, MonitoringBatchResponse
7
+ from ..models import MonitoringBatchRequest, MonitoringBatchResponse, MonitoringEvent
8
8
  from .base import AsyncResource, SyncResource
9
9
 
10
10
 
11
11
  class Ingestion(SyncResource):
12
12
  def ingest_monitoring_events(
13
- self, body: MonitoringBatchRequest, raw: bool = False, **query: Any
13
+ self,
14
+ body: MonitoringEvent | list[MonitoringEvent] | MonitoringBatchRequest,
15
+ raw: bool = False,
16
+ **query: Any,
14
17
  ) -> Any:
15
18
  return self._request(
16
19
  "POST",
@@ -24,7 +27,10 @@ class Ingestion(SyncResource):
24
27
 
25
28
  class AsyncIngestion(AsyncResource):
26
29
  async def ingest_monitoring_events(
27
- self, body: MonitoringBatchRequest, raw: bool = False, **query: Any
30
+ self,
31
+ body: MonitoringEvent | list[MonitoringEvent] | MonitoringBatchRequest,
32
+ raw: bool = False,
33
+ **query: Any,
28
34
  ) -> Any:
29
35
  return await self._request(
30
36
  "POST",
@@ -0,0 +1,57 @@
1
+ """On-chain evidence source registration resource client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from ..errors import ReconifyValidationError
8
+ from ..models import OnchainSourceRequest, OnchainSourceResponse
9
+ from .base import AsyncResource, SyncResource
10
+
11
+
12
+ class Onchain(SyncResource):
13
+ def register_onchain_source(
14
+ self,
15
+ body: OnchainSourceRequest,
16
+ *,
17
+ idempotency_key: str,
18
+ raw: bool = False,
19
+ **query: Any,
20
+ ) -> Any:
21
+ if not idempotency_key or len(idempotency_key) > 200:
22
+ raise ReconifyValidationError("idempotency_key must be between 1 and 200 characters")
23
+ return self._request(
24
+ "POST",
25
+ "/onchain-sources",
26
+ params=query,
27
+ body=body,
28
+ response_model=OnchainSourceResponse,
29
+ raw=raw,
30
+ headers={"Idempotency-Key": idempotency_key},
31
+ )
32
+
33
+ register_source = register_onchain_source
34
+
35
+
36
+ class AsyncOnchain(AsyncResource):
37
+ async def register_onchain_source(
38
+ self,
39
+ body: OnchainSourceRequest,
40
+ *,
41
+ idempotency_key: str,
42
+ raw: bool = False,
43
+ **query: Any,
44
+ ) -> Any:
45
+ if not idempotency_key or len(idempotency_key) > 200:
46
+ raise ReconifyValidationError("idempotency_key must be between 1 and 200 characters")
47
+ return await self._request(
48
+ "POST",
49
+ "/onchain-sources",
50
+ params=query,
51
+ body=body,
52
+ response_model=OnchainSourceResponse,
53
+ raw=raw,
54
+ headers={"Idempotency-Key": idempotency_key},
55
+ )
56
+
57
+ register_source = register_onchain_source
@@ -88,6 +88,8 @@ def _validate_request_body(body: Any) -> Any:
88
88
  return model_dump(body)
89
89
  except ValidationError as exc:
90
90
  raise ReconifyValidationError(str(exc)) from exc
91
+ if isinstance(body, list):
92
+ return [_validate_request_body(item) for item in body]
91
93
  return body
92
94
 
93
95
 
@@ -5,7 +5,7 @@ import json
5
5
  import httpx
6
6
  import pytest
7
7
 
8
- from reconify import Reconify
8
+ from reconify import AsyncReconify, Reconify
9
9
  from reconify.errors import (
10
10
  ReconifyAuthenticationError,
11
11
  ReconifyConflictError,
@@ -19,6 +19,8 @@ from reconify.models import (
19
19
  AddNoteRequest,
20
20
  MonitoringBatchRequest,
21
21
  MonitoringEvent,
22
+ OnchainSourceLocator,
23
+ OnchainSourceRequest,
22
24
  PatchIssueRequest,
23
25
  )
24
26
  from reconify.transport import RetryConfig
@@ -92,6 +94,7 @@ def test_ingestion_serializes_typed_body() -> None:
92
94
  reference="order-1",
93
95
  entity_id="wallet-1",
94
96
  amount="10.00",
97
+ correlation_id="checkout-123",
95
98
  currency="USD",
96
99
  )
97
100
  ]
@@ -104,6 +107,7 @@ def test_ingestion_serializes_typed_body() -> None:
104
107
  client.ingestion.ingest_monitoring_events(body)
105
108
 
106
109
  assert json.loads(requests[0].content)["events"][0]["entity_id"] == "wallet-1"
110
+ assert json.loads(requests[0].content)["events"][0]["correlation_id"] == "checkout-123"
107
111
  assert requests[0].url.path == "/v2/events"
108
112
 
109
113
 
@@ -142,6 +146,116 @@ def test_note_idempotency_header_and_issue_assignment() -> None:
142
146
  assert requests[1].method == "PATCH"
143
147
 
144
148
 
149
+ def test_onchain_source_registration() -> None:
150
+ requests: list[httpx.Request] = []
151
+
152
+ def handler(request: httpx.Request) -> httpx.Response:
153
+ requests.append(request)
154
+ return httpx.Response(
155
+ 202, json={"id": "src-1", "status": "queued", "duplicate": False}, request=request
156
+ )
157
+
158
+ body = OnchainSourceRequest(
159
+ flow="provider_to_settlement_account",
160
+ operation_reference="settlement-1",
161
+ source_event_id="evt_1",
162
+ kind="transaction",
163
+ locator=OnchainSourceLocator(network="ethereum-mainnet", transaction_reference="0xabc"),
164
+ )
165
+ with Reconify(
166
+ "rk_test",
167
+ base_url="http://api.test",
168
+ http_client=httpx.Client(transport=httpx.MockTransport(handler)),
169
+ ) as client:
170
+ result = client.onchain.register_onchain_source(body, idempotency_key="source-1")
171
+
172
+ assert result.id == "src-1"
173
+ assert requests[0].url.path == "/v2/onchain-sources"
174
+ assert requests[0].headers["Idempotency-Key"] == "source-1"
175
+ assert json.loads(requests[0].content)["locator"]["transaction_reference"] == "0xabc"
176
+
177
+
178
+ async def test_async_onchain_source_registration() -> None:
179
+ requests: list[httpx.Request] = []
180
+
181
+ def handler(request: httpx.Request) -> httpx.Response:
182
+ requests.append(request)
183
+ return httpx.Response(
184
+ 202, json={"id": "src-1", "status": "queued", "duplicate": False}, request=request
185
+ )
186
+
187
+ body = OnchainSourceRequest(
188
+ flow="provider_to_settlement_account",
189
+ operation_reference="settlement-1",
190
+ source_event_id="evt_1",
191
+ kind="transaction",
192
+ locator=OnchainSourceLocator(network="ethereum-mainnet"),
193
+ )
194
+ async with AsyncReconify(
195
+ "rk_test",
196
+ base_url="http://api.test",
197
+ http_client=httpx.AsyncClient(transport=httpx.MockTransport(handler)),
198
+ ) as client:
199
+ result = await client.onchain.register_onchain_source(body, idempotency_key="source-1")
200
+
201
+ assert result.status == "queued"
202
+ assert requests[0].headers["Idempotency-Key"] == "source-1"
203
+
204
+
205
+ def test_expanded_event_fields_parse() -> None:
206
+ from reconify.models import Event
207
+
208
+ event = Event.model_validate(
209
+ {
210
+ **_event(),
211
+ "flow": "provider_to_settlement_account",
212
+ "event_type": "settlement.created",
213
+ "entity_type": "settlement_account",
214
+ "causation_id": "evt_prior",
215
+ "correlation_id": "settlement-1",
216
+ "revises_event_id": None,
217
+ "gross": "100.00",
218
+ "net": "98.00",
219
+ "component_count": 1,
220
+ "allocation_count": 0,
221
+ }
222
+ )
223
+
224
+ assert event.event_type.value == "settlement.created"
225
+ assert event.gross == "100.00"
226
+
227
+
228
+ @pytest.mark.parametrize(
229
+ "body",
230
+ [
231
+ MonitoringEvent(
232
+ flow="payment_to_wallet", type="payment.succeeded", reference="r", entity_id="w"
233
+ ),
234
+ [
235
+ MonitoringEvent(
236
+ flow="payment_to_wallet", type="payment.succeeded", reference="r", entity_id="w"
237
+ )
238
+ ],
239
+ ],
240
+ )
241
+ def test_ingestion_accepts_single_and_bare_array(body: object) -> None:
242
+ requests: list[httpx.Request] = []
243
+
244
+ def handler(request: httpx.Request) -> httpx.Response:
245
+ requests.append(request)
246
+ return httpx.Response(202, json={"results": []}, request=request)
247
+
248
+ with Reconify(
249
+ "rk_test",
250
+ base_url="http://api.test",
251
+ http_client=httpx.Client(transport=httpx.MockTransport(handler)),
252
+ ) as client:
253
+ client.ingestion.ingest_monitoring_events(body) # type: ignore[arg-type]
254
+
255
+ payload = json.loads(requests[0].content)
256
+ assert isinstance(payload, (dict, list))
257
+
258
+
145
259
  def test_errors_are_typed_and_safe() -> None:
146
260
  def handler(request: httpx.Request) -> httpx.Response:
147
261
  return httpx.Response(
@@ -177,9 +291,7 @@ def test_errors_are_typed_and_safe() -> None:
177
291
  (503, ReconifyServiceUnavailableError),
178
292
  ],
179
293
  )
180
- def test_public_error_statuses_are_typed(
181
- status: int, error_type: type[Exception]
182
- ) -> None:
294
+ def test_public_error_statuses_are_typed(status: int, error_type: type[Exception]) -> None:
183
295
  def handler(request: httpx.Request) -> httpx.Response:
184
296
  return httpx.Response(
185
297
  status,
@@ -40,7 +40,7 @@ def test_openapi_contains_only_public_routes() -> None:
40
40
 
41
41
  def test_every_openapi_operation_has_a_public_method() -> None:
42
42
  operations = _operations()
43
- assert len(operations) == 13
43
+ assert len(operations) == 14
44
44
  assert len({operation_id for operation_id, _, _ in operations}) == len(operations)
45
45
  assert len(OPERATION_SPECS) == len(operations)
46
46
  for operation_id, verb, route in operations:
@@ -567,7 +567,7 @@ wheels = [
567
567
 
568
568
  [[package]]
569
569
  name = "reconify-python"
570
- version = "1.0.0"
570
+ version = "2.1.0"
571
571
  source = { editable = "." }
572
572
  dependencies = [
573
573
  { name = "httpx" },
@@ -1,4 +0,0 @@
1
- {
2
- "version": "2.0.0",
3
- "sha256": "5cccdf920a62fb325c7fcf9410728889858d156831a78760e41e3f98774db7f2"
4
- }
@@ -1,17 +0,0 @@
1
- # Upgrading to 2.0.0
2
-
3
- The 2.0.0 client targets the public Reconify v2 contract at `/v2`.
4
- Generated operation IDs now use stable `resource_action` identifiers. The
5
- Python resource methods keep their existing snake_case names.
6
-
7
- Use these resources:
8
-
9
- - metadata: API information and health
10
- - events: event listing, lookup, and issue-linked evidence
11
- - ingestion: monitoring event batches
12
- - issues: issue listing, lookup, assignment, notes, and linked data
13
- - organization: organization and member reads
14
-
15
- Python models use snake_case fields and preserve unknown enum values through
16
- tolerant string enums. Existing v1 clients can continue using `/v1`; v2
17
- clients must use the v2 artifact and endpoint.
File without changes