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.
- reconify_python-2.2.0/.openapi-contract.json +4 -0
- {reconify_python-2.1.0 → reconify_python-2.2.0}/CHANGELOG.md +8 -0
- {reconify_python-2.1.0 → reconify_python-2.2.0}/PKG-INFO +7 -3
- {reconify_python-2.1.0 → reconify_python-2.2.0}/README.md +6 -2
- {reconify_python-2.1.0 → reconify_python-2.2.0}/UPGRADING.md +25 -0
- {reconify_python-2.1.0 → reconify_python-2.2.0}/pyproject.toml +1 -1
- {reconify_python-2.1.0 → reconify_python-2.2.0}/src/reconify/__init__.py +1 -1
- {reconify_python-2.1.0 → reconify_python-2.2.0}/src/reconify/client.py +4 -0
- {reconify_python-2.1.0 → reconify_python-2.2.0}/src/reconify/models.py +110 -0
- {reconify_python-2.1.0 → reconify_python-2.2.0}/src/reconify/resources/__init__.py +7 -0
- {reconify_python-2.1.0 → reconify_python-2.2.0}/src/reconify/resources/ingestion.py +9 -3
- reconify_python-2.2.0/src/reconify/resources/onchain.py +57 -0
- {reconify_python-2.1.0 → reconify_python-2.2.0}/src/reconify/transport.py +2 -0
- {reconify_python-2.1.0 → reconify_python-2.2.0}/tests/test_client.py +114 -4
- {reconify_python-2.1.0 → reconify_python-2.2.0}/tests/test_openapi_coverage.py +1 -1
- reconify_python-2.1.0/.openapi-contract.json +0 -4
- {reconify_python-2.1.0 → reconify_python-2.2.0}/.bumpversion.cfg +0 -0
- {reconify_python-2.1.0 → reconify_python-2.2.0}/.conductor/settings.toml +0 -0
- {reconify_python-2.1.0 → reconify_python-2.2.0}/.github/workflows/ci.yml +0 -0
- {reconify_python-2.1.0 → reconify_python-2.2.0}/.github/workflows/release.yml +0 -0
- {reconify_python-2.1.0 → reconify_python-2.2.0}/.github/workflows/sync-contract.yml +0 -0
- {reconify_python-2.1.0 → reconify_python-2.2.0}/.gitignore +0 -0
- {reconify_python-2.1.0 → reconify_python-2.2.0}/AGENTS.md +0 -0
- {reconify_python-2.1.0 → reconify_python-2.2.0}/LICENSE +0 -0
- {reconify_python-2.1.0 → reconify_python-2.2.0}/context7.json +0 -0
- {reconify_python-2.1.0 → reconify_python-2.2.0}/reports/api-error-data-report.md +0 -0
- {reconify_python-2.1.0 → reconify_python-2.2.0}/scripts/fetch_contract.py +0 -0
- {reconify_python-2.1.0 → reconify_python-2.2.0}/src/reconify/errors.py +0 -0
- {reconify_python-2.1.0 → reconify_python-2.2.0}/src/reconify/pagination.py +0 -0
- {reconify_python-2.1.0 → reconify_python-2.2.0}/src/reconify/py.typed +0 -0
- {reconify_python-2.1.0 → reconify_python-2.2.0}/src/reconify/resources/base.py +0 -0
- {reconify_python-2.1.0 → reconify_python-2.2.0}/src/reconify/resources/events.py +0 -0
- {reconify_python-2.1.0 → reconify_python-2.2.0}/src/reconify/resources/issues.py +0 -0
- {reconify_python-2.1.0 → reconify_python-2.2.0}/src/reconify/resources/metadata.py +0 -0
- {reconify_python-2.1.0 → reconify_python-2.2.0}/src/reconify/resources/organization.py +0 -0
- {reconify_python-2.1.0 → reconify_python-2.2.0}/tests/test_pagination.py +0 -0
- {reconify_python-2.1.0 → reconify_python-2.2.0}/uv.lock +0 -0
|
@@ -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.
|
|
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,
|
|
45
|
-
current public contract contains exactly
|
|
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,
|
|
27
|
-
current public contract contains exactly
|
|
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
|
+
```
|
|
@@ -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,
|
|
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,
|
|
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) ==
|
|
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:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|