reconify-python 2.1.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 (37) hide show
  1. reconify_python-2.2.0/.openapi-contract.json +4 -0
  2. {reconify_python-2.1.0 → reconify_python-2.2.0}/CHANGELOG.md +8 -0
  3. {reconify_python-2.1.0 → reconify_python-2.2.0}/PKG-INFO +7 -3
  4. {reconify_python-2.1.0 → reconify_python-2.2.0}/README.md +6 -2
  5. {reconify_python-2.1.0 → reconify_python-2.2.0}/UPGRADING.md +25 -0
  6. {reconify_python-2.1.0 → reconify_python-2.2.0}/pyproject.toml +1 -1
  7. {reconify_python-2.1.0 → reconify_python-2.2.0}/src/reconify/__init__.py +1 -1
  8. {reconify_python-2.1.0 → reconify_python-2.2.0}/src/reconify/client.py +4 -0
  9. {reconify_python-2.1.0 → reconify_python-2.2.0}/src/reconify/models.py +110 -0
  10. {reconify_python-2.1.0 → reconify_python-2.2.0}/src/reconify/resources/__init__.py +7 -0
  11. {reconify_python-2.1.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.1.0 → reconify_python-2.2.0}/src/reconify/transport.py +2 -0
  14. {reconify_python-2.1.0 → reconify_python-2.2.0}/tests/test_client.py +114 -4
  15. {reconify_python-2.1.0 → reconify_python-2.2.0}/tests/test_openapi_coverage.py +1 -1
  16. reconify_python-2.1.0/.openapi-contract.json +0 -4
  17. {reconify_python-2.1.0 → reconify_python-2.2.0}/.bumpversion.cfg +0 -0
  18. {reconify_python-2.1.0 → reconify_python-2.2.0}/.conductor/settings.toml +0 -0
  19. {reconify_python-2.1.0 → reconify_python-2.2.0}/.github/workflows/ci.yml +0 -0
  20. {reconify_python-2.1.0 → reconify_python-2.2.0}/.github/workflows/release.yml +0 -0
  21. {reconify_python-2.1.0 → reconify_python-2.2.0}/.github/workflows/sync-contract.yml +0 -0
  22. {reconify_python-2.1.0 → reconify_python-2.2.0}/.gitignore +0 -0
  23. {reconify_python-2.1.0 → reconify_python-2.2.0}/AGENTS.md +0 -0
  24. {reconify_python-2.1.0 → reconify_python-2.2.0}/LICENSE +0 -0
  25. {reconify_python-2.1.0 → reconify_python-2.2.0}/context7.json +0 -0
  26. {reconify_python-2.1.0 → reconify_python-2.2.0}/reports/api-error-data-report.md +0 -0
  27. {reconify_python-2.1.0 → reconify_python-2.2.0}/scripts/fetch_contract.py +0 -0
  28. {reconify_python-2.1.0 → reconify_python-2.2.0}/src/reconify/errors.py +0 -0
  29. {reconify_python-2.1.0 → reconify_python-2.2.0}/src/reconify/pagination.py +0 -0
  30. {reconify_python-2.1.0 → reconify_python-2.2.0}/src/reconify/py.typed +0 -0
  31. {reconify_python-2.1.0 → reconify_python-2.2.0}/src/reconify/resources/base.py +0 -0
  32. {reconify_python-2.1.0 → reconify_python-2.2.0}/src/reconify/resources/events.py +0 -0
  33. {reconify_python-2.1.0 → reconify_python-2.2.0}/src/reconify/resources/issues.py +0 -0
  34. {reconify_python-2.1.0 → reconify_python-2.2.0}/src/reconify/resources/metadata.py +0 -0
  35. {reconify_python-2.1.0 → reconify_python-2.2.0}/src/reconify/resources/organization.py +0 -0
  36. {reconify_python-2.1.0 → reconify_python-2.2.0}/tests/test_pagination.py +0 -0
  37. {reconify_python-2.1.0 → reconify_python-2.2.0}/uv.lock +0 -0
@@ -0,0 +1,4 @@
1
+ {
2
+ "version": "2.3.0",
3
+ "sha256": "cba4133401cc2364b4de7f270c4de43f151775be4d8930232888f2f5e70b6920"
4
+ }
@@ -1,5 +1,13 @@
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
+
3
11
  ## 2.1.0
4
12
 
5
13
  - Added optional `correlation_id` support to the typed monitoring event request
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: reconify-python
3
- Version: 2.1.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
@@ -34,3 +34,28 @@ MonitoringEvent(
34
34
 
35
35
  The value is sent as event metadata and does not change operation grouping or
36
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.1.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):
@@ -132,8 +205,10 @@ class MonitoringEvent(RequestModel):
132
205
  occurred_at: datetime | None = None
133
206
  amount: str | None = None
134
207
  correlation_id: str | None = None
208
+ causation_id: str | None = None
135
209
  currency: str | None = None
136
210
  data: MonitoringEventData | None = None
211
+ financial_breakdown: FinancialBreakdown | None = None
137
212
  metadata: dict[str, str | int | float | bool] | None = None
138
213
 
139
214
 
@@ -172,8 +247,17 @@ class Event(ResponseModel):
172
247
  received_at: datetime
173
248
  amount: str | None = None
174
249
  currency: str | None = None
250
+ causation_id: str | None = None
251
+ correlation_id: str | None = None
252
+ revises_event_id: str | None = None
175
253
  provider: str | None = None
176
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
177
261
 
178
262
 
179
263
  class ListEventsResponse(ResponseModel):
@@ -244,5 +328,31 @@ class Error(ResponseModel):
244
328
  detail: str | None = None
245
329
 
246
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
+
247
357
  def model_dump(model: BaseModel) -> dict[str, Any]:
248
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
@@ -144,6 +146,116 @@ def test_note_idempotency_header_and_issue_assignment() -> None:
144
146
  assert requests[1].method == "PATCH"
145
147
 
146
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
+
147
259
  def test_errors_are_typed_and_safe() -> None:
148
260
  def handler(request: httpx.Request) -> httpx.Response:
149
261
  return httpx.Response(
@@ -179,9 +291,7 @@ def test_errors_are_typed_and_safe() -> None:
179
291
  (503, ReconifyServiceUnavailableError),
180
292
  ],
181
293
  )
182
- def test_public_error_statuses_are_typed(
183
- status: int, error_type: type[Exception]
184
- ) -> None:
294
+ def test_public_error_statuses_are_typed(status: int, error_type: type[Exception]) -> None:
185
295
  def handler(request: httpx.Request) -> httpx.Response:
186
296
  return httpx.Response(
187
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:
@@ -1,4 +0,0 @@
1
- {
2
- "version": "2.0.0",
3
- "sha256": "5cccdf920a62fb325c7fcf9410728889858d156831a78760e41e3f98774db7f2"
4
- }
File without changes
File without changes