reconify-python 0.1.0__py3-none-any.whl
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/__init__.py +36 -0
- reconify/client.py +264 -0
- reconify/errors.py +132 -0
- reconify/models.py +1067 -0
- reconify/pagination.py +111 -0
- reconify/py.typed +0 -0
- reconify/resources/__init__.py +124 -0
- reconify/resources/alerts.py +58 -0
- reconify/resources/base.py +96 -0
- reconify/resources/events.py +79 -0
- reconify/resources/ingestion.py +94 -0
- reconify/resources/issues.py +207 -0
- reconify/resources/ledger.py +205 -0
- reconify/resources/reconciliations.py +236 -0
- reconify/resources/search.py +37 -0
- reconify/resources/setup.py +311 -0
- reconify/resources/transactions.py +58 -0
- reconify/resources/wallets.py +79 -0
- reconify/transport.py +299 -0
- reconify_python-0.1.0.dist-info/METADATA +125 -0
- reconify_python-0.1.0.dist-info/RECORD +23 -0
- reconify_python-0.1.0.dist-info/WHEEL +4 -0
- reconify_python-0.1.0.dist-info/licenses/LICENSE +21 -0
reconify/__init__.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Typed clients for the Reconify Public API."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
from .client import AsyncReconify, Reconify
|
|
6
|
+
from .errors import (
|
|
7
|
+
ReconifyAuthenticationError,
|
|
8
|
+
ReconifyConflictError,
|
|
9
|
+
ReconifyError,
|
|
10
|
+
ReconifyNotFoundError,
|
|
11
|
+
ReconifyPermissionError,
|
|
12
|
+
ReconifyRateLimitError,
|
|
13
|
+
ReconifyRequestError,
|
|
14
|
+
ReconifyServerError,
|
|
15
|
+
ReconifyServiceUnavailableError,
|
|
16
|
+
ReconifyValidationError,
|
|
17
|
+
)
|
|
18
|
+
from .transport import RawResponse, RetryConfig
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"AsyncReconify",
|
|
22
|
+
"Reconify",
|
|
23
|
+
"__version__",
|
|
24
|
+
"RawResponse",
|
|
25
|
+
"RetryConfig",
|
|
26
|
+
"ReconifyError",
|
|
27
|
+
"ReconifyValidationError",
|
|
28
|
+
"ReconifyAuthenticationError",
|
|
29
|
+
"ReconifyPermissionError",
|
|
30
|
+
"ReconifyNotFoundError",
|
|
31
|
+
"ReconifyConflictError",
|
|
32
|
+
"ReconifyRequestError",
|
|
33
|
+
"ReconifyRateLimitError",
|
|
34
|
+
"ReconifyServiceUnavailableError",
|
|
35
|
+
"ReconifyServerError",
|
|
36
|
+
]
|
reconify/client.py
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""Top-level synchronous and asynchronous Reconify clients."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from collections.abc import AsyncIterator, Iterator
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from .errors import ReconifyValidationError
|
|
12
|
+
from .pagination import (
|
|
13
|
+
aiter_cursor_pages,
|
|
14
|
+
aiter_offset_pages,
|
|
15
|
+
iter_cursor_pages,
|
|
16
|
+
iter_offset_pages,
|
|
17
|
+
)
|
|
18
|
+
from .resources import (
|
|
19
|
+
ASYNC_RESOURCE_CLASSES,
|
|
20
|
+
SYNC_RESOURCE_CLASSES,
|
|
21
|
+
Alerts,
|
|
22
|
+
AsyncAlerts,
|
|
23
|
+
AsyncEvents,
|
|
24
|
+
AsyncIngestion,
|
|
25
|
+
AsyncIssues,
|
|
26
|
+
AsyncLedger,
|
|
27
|
+
AsyncReconciliations,
|
|
28
|
+
AsyncSearch,
|
|
29
|
+
AsyncSetup,
|
|
30
|
+
AsyncTransactions,
|
|
31
|
+
AsyncWallets,
|
|
32
|
+
Events,
|
|
33
|
+
Ingestion,
|
|
34
|
+
Issues,
|
|
35
|
+
Ledger,
|
|
36
|
+
Reconciliations,
|
|
37
|
+
Search,
|
|
38
|
+
Setup,
|
|
39
|
+
Transactions,
|
|
40
|
+
Wallets,
|
|
41
|
+
)
|
|
42
|
+
from .transport import AsyncTransport, RetryConfig, SyncTransport
|
|
43
|
+
|
|
44
|
+
DEFAULT_BASE_URL = "https://api.reconifyhq.com/v1"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _normalize_base_url(base_url: str | None) -> str:
|
|
48
|
+
value = (base_url or os.getenv("RECONIFY_API_URL") or DEFAULT_BASE_URL).rstrip("/")
|
|
49
|
+
return value if value.endswith("/v1") else f"{value}/v1"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _api_key(api_key: str | None) -> str:
|
|
53
|
+
value = api_key or os.getenv("RECONIFY_API_KEY")
|
|
54
|
+
if not value:
|
|
55
|
+
raise ReconifyValidationError("An API key is required")
|
|
56
|
+
if value.startswith("sk_live_"):
|
|
57
|
+
raise ReconifyValidationError("Legacy sk_live_ keys are not supported; use an rk_ key")
|
|
58
|
+
if not value.startswith("rk_"):
|
|
59
|
+
raise ReconifyValidationError("Reconify public API keys must start with rk_")
|
|
60
|
+
return value
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Reconify:
|
|
64
|
+
"""Synchronous typed Reconify API client."""
|
|
65
|
+
|
|
66
|
+
alerts: Alerts
|
|
67
|
+
events: Events
|
|
68
|
+
ingestion: Ingestion
|
|
69
|
+
issues: Issues
|
|
70
|
+
ledger: Ledger
|
|
71
|
+
reconciliations: Reconciliations
|
|
72
|
+
search: Search
|
|
73
|
+
setup: Setup
|
|
74
|
+
transactions: Transactions
|
|
75
|
+
wallets: Wallets
|
|
76
|
+
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
api_key: str | None = None,
|
|
80
|
+
*,
|
|
81
|
+
base_url: str | None = None,
|
|
82
|
+
timeout: float | httpx.Timeout = 30.0,
|
|
83
|
+
request_id: str | None = None,
|
|
84
|
+
retry: RetryConfig | None = None,
|
|
85
|
+
http_client: httpx.Client | None = None,
|
|
86
|
+
) -> None:
|
|
87
|
+
self._transport = SyncTransport(
|
|
88
|
+
base_url=_normalize_base_url(base_url),
|
|
89
|
+
api_key=_api_key(api_key),
|
|
90
|
+
timeout=timeout,
|
|
91
|
+
request_id=request_id,
|
|
92
|
+
retry=retry,
|
|
93
|
+
client=http_client,
|
|
94
|
+
)
|
|
95
|
+
for name, resource_class in SYNC_RESOURCE_CLASSES.items():
|
|
96
|
+
setattr(self, name, resource_class(self._transport))
|
|
97
|
+
|
|
98
|
+
def close(self) -> None:
|
|
99
|
+
self._transport.close()
|
|
100
|
+
|
|
101
|
+
def __enter__(self) -> Reconify:
|
|
102
|
+
return self
|
|
103
|
+
|
|
104
|
+
def __exit__(self, *_: Any) -> None:
|
|
105
|
+
self.close()
|
|
106
|
+
|
|
107
|
+
def iter_events(self, **query: Any) -> Iterator[Any]:
|
|
108
|
+
return iter_cursor_pages(
|
|
109
|
+
lambda params: self.events.list_events(**params), item_field="events", query=query
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def iter_issues(self, **query: Any) -> Iterator[Any]:
|
|
113
|
+
return iter_cursor_pages(
|
|
114
|
+
lambda params: self.issues.list_issues(**params), item_field="issues", query=query
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def iter_ledger_sources(self, **query: Any) -> Iterator[Any]:
|
|
118
|
+
return iter_offset_pages(
|
|
119
|
+
lambda params: self.ledger.list_ledger_sources(**params),
|
|
120
|
+
item_field="sources",
|
|
121
|
+
query=query,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def iter_ledger_transactions(self, source_id: str, **query: Any) -> Iterator[Any]:
|
|
125
|
+
return iter_offset_pages(
|
|
126
|
+
lambda params: self.ledger.list_transactions(source_id, **params),
|
|
127
|
+
item_field="transactions",
|
|
128
|
+
query=query,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def iter_reconciliation_schedules(self, **query: Any) -> Iterator[Any]:
|
|
132
|
+
return iter_offset_pages(
|
|
133
|
+
lambda params: self.reconciliations.list_reconciliation_schedules(**params),
|
|
134
|
+
item_field="schedules",
|
|
135
|
+
query=query,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def iter_reconciliations(self, **query: Any) -> Iterator[Any]:
|
|
139
|
+
return iter_offset_pages(
|
|
140
|
+
lambda params: self.reconciliations.list_reconciliations(**params),
|
|
141
|
+
item_field="reconciliations",
|
|
142
|
+
query=query,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def iter_setup_sources(self, **query: Any) -> Iterator[Any]:
|
|
146
|
+
return iter_offset_pages(
|
|
147
|
+
lambda params: self.setup.list_setup_sources(**params),
|
|
148
|
+
item_field="sources",
|
|
149
|
+
query=query,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def iter_wallet_transactions(self, **query: Any) -> Iterator[Any]:
|
|
153
|
+
return iter_cursor_pages(
|
|
154
|
+
lambda params: self.transactions.list_wallet_transactions(**params),
|
|
155
|
+
item_field="transactions",
|
|
156
|
+
query=query,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def iter_wallets(self, **query: Any) -> Iterator[Any]:
|
|
160
|
+
return iter_offset_pages(
|
|
161
|
+
lambda params: self.wallets.list_wallets(**params), item_field="wallets", query=query
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class AsyncReconify:
|
|
166
|
+
"""Asynchronous typed Reconify API client."""
|
|
167
|
+
|
|
168
|
+
alerts: AsyncAlerts
|
|
169
|
+
events: AsyncEvents
|
|
170
|
+
ingestion: AsyncIngestion
|
|
171
|
+
issues: AsyncIssues
|
|
172
|
+
ledger: AsyncLedger
|
|
173
|
+
reconciliations: AsyncReconciliations
|
|
174
|
+
search: AsyncSearch
|
|
175
|
+
setup: AsyncSetup
|
|
176
|
+
transactions: AsyncTransactions
|
|
177
|
+
wallets: AsyncWallets
|
|
178
|
+
|
|
179
|
+
def __init__(
|
|
180
|
+
self,
|
|
181
|
+
api_key: str | None = None,
|
|
182
|
+
*,
|
|
183
|
+
base_url: str | None = None,
|
|
184
|
+
timeout: float | httpx.Timeout = 30.0,
|
|
185
|
+
request_id: str | None = None,
|
|
186
|
+
retry: RetryConfig | None = None,
|
|
187
|
+
http_client: httpx.AsyncClient | None = None,
|
|
188
|
+
) -> None:
|
|
189
|
+
self._transport = AsyncTransport(
|
|
190
|
+
base_url=_normalize_base_url(base_url),
|
|
191
|
+
api_key=_api_key(api_key),
|
|
192
|
+
timeout=timeout,
|
|
193
|
+
request_id=request_id,
|
|
194
|
+
retry=retry,
|
|
195
|
+
client=http_client,
|
|
196
|
+
)
|
|
197
|
+
for name, resource_class in ASYNC_RESOURCE_CLASSES.items():
|
|
198
|
+
setattr(self, name, resource_class(self._transport))
|
|
199
|
+
|
|
200
|
+
async def aclose(self) -> None:
|
|
201
|
+
await self._transport.aclose()
|
|
202
|
+
|
|
203
|
+
async def __aenter__(self) -> AsyncReconify:
|
|
204
|
+
return self
|
|
205
|
+
|
|
206
|
+
async def __aexit__(self, *_: Any) -> None:
|
|
207
|
+
await self.aclose()
|
|
208
|
+
|
|
209
|
+
def iter_events(self, **query: Any) -> AsyncIterator[Any]:
|
|
210
|
+
return aiter_cursor_pages(
|
|
211
|
+
lambda params: self.events.list_events(**params), item_field="events", query=query
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
def iter_issues(self, **query: Any) -> AsyncIterator[Any]:
|
|
215
|
+
return aiter_cursor_pages(
|
|
216
|
+
lambda params: self.issues.list_issues(**params), item_field="issues", query=query
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
def iter_ledger_sources(self, **query: Any) -> AsyncIterator[Any]:
|
|
220
|
+
return aiter_offset_pages(
|
|
221
|
+
lambda params: self.ledger.list_ledger_sources(**params),
|
|
222
|
+
item_field="sources",
|
|
223
|
+
query=query,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
def iter_ledger_transactions(self, source_id: str, **query: Any) -> AsyncIterator[Any]:
|
|
227
|
+
return aiter_offset_pages(
|
|
228
|
+
lambda params: self.ledger.list_transactions(source_id, **params),
|
|
229
|
+
item_field="transactions",
|
|
230
|
+
query=query,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def iter_reconciliation_schedules(self, **query: Any) -> AsyncIterator[Any]:
|
|
234
|
+
return aiter_offset_pages(
|
|
235
|
+
lambda params: self.reconciliations.list_reconciliation_schedules(**params),
|
|
236
|
+
item_field="schedules",
|
|
237
|
+
query=query,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
def iter_reconciliations(self, **query: Any) -> AsyncIterator[Any]:
|
|
241
|
+
return aiter_offset_pages(
|
|
242
|
+
lambda params: self.reconciliations.list_reconciliations(**params),
|
|
243
|
+
item_field="reconciliations",
|
|
244
|
+
query=query,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
def iter_setup_sources(self, **query: Any) -> AsyncIterator[Any]:
|
|
248
|
+
return aiter_offset_pages(
|
|
249
|
+
lambda params: self.setup.list_setup_sources(**params),
|
|
250
|
+
item_field="sources",
|
|
251
|
+
query=query,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
def iter_wallet_transactions(self, **query: Any) -> AsyncIterator[Any]:
|
|
255
|
+
return aiter_cursor_pages(
|
|
256
|
+
lambda params: self.transactions.list_wallet_transactions(**params),
|
|
257
|
+
item_field="transactions",
|
|
258
|
+
query=query,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
def iter_wallets(self, **query: Any) -> AsyncIterator[Any]:
|
|
262
|
+
return aiter_offset_pages(
|
|
263
|
+
lambda params: self.wallets.list_wallets(**params), item_field="wallets", query=query
|
|
264
|
+
)
|
reconify/errors.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Exception types and safe error parsing for Reconify responses."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, cast
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ReconifyError(Exception):
|
|
9
|
+
"""Base class for client, validation, transport, and API errors."""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
message: str,
|
|
14
|
+
*,
|
|
15
|
+
status_code: int | None = None,
|
|
16
|
+
title: str | None = None,
|
|
17
|
+
detail: str | None = None,
|
|
18
|
+
code: str | None = None,
|
|
19
|
+
validation_errors: list[dict[str, Any]] | None = None,
|
|
20
|
+
request_id: str | None = None,
|
|
21
|
+
response_headers: dict[str, str] | None = None,
|
|
22
|
+
response_metadata: dict[str, Any] | None = None,
|
|
23
|
+
) -> None:
|
|
24
|
+
super().__init__(message)
|
|
25
|
+
self.status_code = status_code
|
|
26
|
+
self.title = title
|
|
27
|
+
self.detail = detail
|
|
28
|
+
self.code = code
|
|
29
|
+
self.validation_errors = validation_errors or []
|
|
30
|
+
self.request_id = request_id
|
|
31
|
+
self.response_headers = response_headers or {}
|
|
32
|
+
self.response_metadata = response_metadata or {}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ReconifyValidationError(ReconifyError):
|
|
36
|
+
"""The client rejected invalid arguments before making a request."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ReconifyAuthenticationError(ReconifyError):
|
|
40
|
+
"""The API key is missing or not accepted (HTTP 401)."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ReconifyPermissionError(ReconifyError):
|
|
44
|
+
"""The API key cannot access the requested resource (HTTP 403)."""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ReconifyNotFoundError(ReconifyError):
|
|
48
|
+
"""The requested resource does not exist (HTTP 404)."""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ReconifyConflictError(ReconifyError):
|
|
52
|
+
"""The request conflicts with current server state (HTTP 409)."""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ReconifyRequestError(ReconifyError):
|
|
56
|
+
"""The request is malformed or fails validation (HTTP 400/422)."""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ReconifyRateLimitError(ReconifyError):
|
|
60
|
+
"""The API rate limit was exceeded (HTTP 429)."""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ReconifyServiceUnavailableError(ReconifyError):
|
|
64
|
+
"""The API is temporarily unavailable (HTTP 503)."""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ReconifyServerError(ReconifyError):
|
|
68
|
+
"""The API returned an unexpected 5xx response."""
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
_STATUS_ERRORS: dict[int, type[ReconifyError]] = {
|
|
72
|
+
400: ReconifyRequestError,
|
|
73
|
+
401: ReconifyAuthenticationError,
|
|
74
|
+
403: ReconifyPermissionError,
|
|
75
|
+
404: ReconifyNotFoundError,
|
|
76
|
+
409: ReconifyConflictError,
|
|
77
|
+
422: ReconifyRequestError,
|
|
78
|
+
429: ReconifyRateLimitError,
|
|
79
|
+
500: ReconifyServerError,
|
|
80
|
+
503: ReconifyServiceUnavailableError,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def error_for_response(
|
|
85
|
+
status_code: int,
|
|
86
|
+
headers: dict[str, str],
|
|
87
|
+
payload: Any,
|
|
88
|
+
*,
|
|
89
|
+
request_id: str | None,
|
|
90
|
+
) -> ReconifyError:
|
|
91
|
+
"""Build a typed exception without including credentials or request bodies."""
|
|
92
|
+
|
|
93
|
+
data = payload if isinstance(payload, dict) else {}
|
|
94
|
+
title = data.get("title") if isinstance(data.get("title"), str) else None
|
|
95
|
+
detail = data.get("detail") if isinstance(data.get("detail"), str) else None
|
|
96
|
+
if detail is None and isinstance(data.get("error"), str):
|
|
97
|
+
detail = data["error"]
|
|
98
|
+
if detail is None and isinstance(data.get("message"), str):
|
|
99
|
+
detail = data["message"]
|
|
100
|
+
|
|
101
|
+
validation = data.get("errors") if isinstance(data.get("errors"), list) else []
|
|
102
|
+
validation = cast(list[Any], validation)
|
|
103
|
+
safe_validation = [item for item in validation if isinstance(item, dict)]
|
|
104
|
+
code = None
|
|
105
|
+
for item in safe_validation:
|
|
106
|
+
if item.get("location") == "$code" and isinstance(item.get("message"), str):
|
|
107
|
+
code = item["message"]
|
|
108
|
+
break
|
|
109
|
+
if code is None and isinstance(data.get("code"), str):
|
|
110
|
+
code = data["code"]
|
|
111
|
+
|
|
112
|
+
message = detail or title or f"Reconify API request failed with status {status_code}"
|
|
113
|
+
exception_type = _STATUS_ERRORS.get(
|
|
114
|
+
status_code,
|
|
115
|
+
ReconifyServerError if status_code >= 500 else ReconifyError,
|
|
116
|
+
)
|
|
117
|
+
metadata = {
|
|
118
|
+
key: value
|
|
119
|
+
for key, value in data.items()
|
|
120
|
+
if key not in {"detail", "message", "error", "errors"}
|
|
121
|
+
}
|
|
122
|
+
return exception_type(
|
|
123
|
+
message,
|
|
124
|
+
status_code=status_code,
|
|
125
|
+
title=title,
|
|
126
|
+
detail=detail,
|
|
127
|
+
code=code,
|
|
128
|
+
validation_errors=safe_validation,
|
|
129
|
+
request_id=request_id,
|
|
130
|
+
response_headers=headers,
|
|
131
|
+
response_metadata=metadata,
|
|
132
|
+
)
|