cyrus-payments 0.1.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.
- cyrus_payments-0.1.0/.gitignore +10 -0
- cyrus_payments-0.1.0/PKG-INFO +11 -0
- cyrus_payments-0.1.0/pyproject.toml +26 -0
- cyrus_payments-0.1.0/src/cyrus/__init__.py +43 -0
- cyrus_payments-0.1.0/src/cyrus/_enums.py +64 -0
- cyrus_payments-0.1.0/src/cyrus/_exceptions.py +45 -0
- cyrus_payments-0.1.0/src/cyrus/_http.py +126 -0
- cyrus_payments-0.1.0/src/cyrus/_pagination.py +86 -0
- cyrus_payments-0.1.0/src/cyrus/_utils.py +15 -0
- cyrus_payments-0.1.0/src/cyrus/client.py +45 -0
- cyrus_payments-0.1.0/src/cyrus/models/__init__.py +39 -0
- cyrus_payments-0.1.0/src/cyrus/models/requests.py +43 -0
- cyrus_payments-0.1.0/src/cyrus/models/responses.py +142 -0
- cyrus_payments-0.1.0/src/cyrus/py.typed +0 -0
- cyrus_payments-0.1.0/src/cyrus/resources/__init__.py +5 -0
- cyrus_payments-0.1.0/src/cyrus/resources/customers.py +113 -0
- cyrus_payments-0.1.0/src/cyrus/resources/payment_events.py +60 -0
- cyrus_payments-0.1.0/src/cyrus/resources/transactions.py +78 -0
- cyrus_payments-0.1.0/tests/__init__.py +0 -0
- cyrus_payments-0.1.0/tests/resources/__init__.py +0 -0
- cyrus_payments-0.1.0/tests/test_client.py +424 -0
- cyrus_payments-0.1.0/uv.lock +412 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cyrus-payments
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the Cyrus Payments API
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: httpx<1,>=0.27
|
|
7
|
+
Requires-Dist: pydantic<3,>=2.0
|
|
8
|
+
Provides-Extra: dev
|
|
9
|
+
Requires-Dist: pytest-asyncio<1,>=0.24; extra == 'dev'
|
|
10
|
+
Requires-Dist: pytest<9,>=8; extra == 'dev'
|
|
11
|
+
Requires-Dist: respx<1,>=0.22; extra == 'dev'
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "cyrus-payments"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Python SDK for the Cyrus Payments API"
|
|
5
|
+
requires-python = ">=3.10"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"httpx>=0.27,<1",
|
|
8
|
+
"pydantic>=2.0,<3",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
[project.optional-dependencies]
|
|
12
|
+
dev = [
|
|
13
|
+
"pytest>=8,<9",
|
|
14
|
+
"pytest-asyncio>=0.24,<1",
|
|
15
|
+
"respx>=0.22,<1",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[build-system]
|
|
19
|
+
requires = ["hatchling"]
|
|
20
|
+
build-backend = "hatchling.build"
|
|
21
|
+
|
|
22
|
+
[tool.hatch.build.targets.wheel]
|
|
23
|
+
packages = ["src/cyrus"]
|
|
24
|
+
|
|
25
|
+
[tool.pytest.ini_options]
|
|
26
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Python SDK for the Cyrus Payments API."""
|
|
2
|
+
|
|
3
|
+
from cyrus.client import CyrusClient
|
|
4
|
+
from cyrus._enums import (
|
|
5
|
+
CustomerStatus,
|
|
6
|
+
KycTier,
|
|
7
|
+
MatchStatus,
|
|
8
|
+
PaymentEventStatus,
|
|
9
|
+
PaymentEventType,
|
|
10
|
+
ReconciliationFailureReason,
|
|
11
|
+
TransactionStatus,
|
|
12
|
+
TransactionType,
|
|
13
|
+
)
|
|
14
|
+
from cyrus._exceptions import (
|
|
15
|
+
ConflictError,
|
|
16
|
+
CyrusAPIError,
|
|
17
|
+
NotFoundError,
|
|
18
|
+
ProviderError,
|
|
19
|
+
ServerError,
|
|
20
|
+
UnauthorizedError,
|
|
21
|
+
ValidationError,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"CyrusClient",
|
|
26
|
+
"ConflictError",
|
|
27
|
+
"CustomerStatus",
|
|
28
|
+
"CyrusAPIError",
|
|
29
|
+
"KycTier",
|
|
30
|
+
"MatchStatus",
|
|
31
|
+
"NotFoundError",
|
|
32
|
+
"PaymentEventStatus",
|
|
33
|
+
"PaymentEventType",
|
|
34
|
+
"ProviderError",
|
|
35
|
+
"ReconciliationFailureReason",
|
|
36
|
+
"ServerError",
|
|
37
|
+
"TransactionStatus",
|
|
38
|
+
"TransactionType",
|
|
39
|
+
"UnauthorizedError",
|
|
40
|
+
"ValidationError",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TransactionType(str, Enum):
|
|
5
|
+
CUSTOMER_PAYMENT = "CUSTOMER_PAYMENT"
|
|
6
|
+
PAYOUT = "PAYOUT"
|
|
7
|
+
REVERSAL = "REVERSAL"
|
|
8
|
+
ADJUSTMENT = "ADJUSTMENT"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TransactionStatus(str, Enum):
|
|
12
|
+
PENDING = "PENDING"
|
|
13
|
+
SUCCESSFUL = "SUCCESSFUL"
|
|
14
|
+
FAILED = "FAILED"
|
|
15
|
+
REVERSED = "REVERSED"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MatchStatus(str, Enum):
|
|
19
|
+
MATCHED = "MATCHED"
|
|
20
|
+
UNMATCHED = "UNMATCHED"
|
|
21
|
+
DISCREPANCY = "DISCREPANCY"
|
|
22
|
+
ORPHANED = "ORPHANED"
|
|
23
|
+
MANUAL_REVIEW = "MANUAL_REVIEW"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CustomerStatus(str, Enum):
|
|
27
|
+
ACTIVE = "ACTIVE"
|
|
28
|
+
SUSPENDED = "SUSPENDED"
|
|
29
|
+
CLOSED = "CLOSED"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class KycTier(str, Enum):
|
|
33
|
+
TIER_1 = "TIER_1"
|
|
34
|
+
TIER_2 = "TIER_2"
|
|
35
|
+
TIER_3 = "TIER_3"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class PaymentEventType(str, Enum):
|
|
39
|
+
PAYMENT_SUCCESS = "PAYMENT_SUCCESS"
|
|
40
|
+
PAYMENT_FAILED = "PAYMENT_FAILED"
|
|
41
|
+
PAYMENT_REVERSED = "PAYMENT_REVERSED"
|
|
42
|
+
PAYOUT_SUCCESS = "PAYOUT_SUCCESS"
|
|
43
|
+
PAYOUT_FAILED = "PAYOUT_FAILED"
|
|
44
|
+
PAYOUT_REFUND = "PAYOUT_REFUND"
|
|
45
|
+
UNKNOWN = "UNKNOWN"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class PaymentEventStatus(str, Enum):
|
|
49
|
+
RECEIVED = "RECEIVED"
|
|
50
|
+
PROCESSED = "PROCESSED"
|
|
51
|
+
IGNORED = "IGNORED"
|
|
52
|
+
PROCESSED_DUPLICATE = "PROCESSED_DUPLICATE"
|
|
53
|
+
FAILED = "FAILED"
|
|
54
|
+
REATTRIBUTED = "REATTRIBUTED"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ReconciliationFailureReason(str, Enum):
|
|
58
|
+
UNKNOWN_VIRTUAL_ACCOUNT = "UNKNOWN_VIRTUAL_ACCOUNT"
|
|
59
|
+
INACTIVE_CUSTOMER = "INACTIVE_CUSTOMER"
|
|
60
|
+
NON_CREDIT_EVENT = "NON_CREDIT_EVENT"
|
|
61
|
+
DUPLICATE = "DUPLICATE"
|
|
62
|
+
SIGNATURE_MISMATCH = "SIGNATURE_MISMATCH"
|
|
63
|
+
AMOUNT_MISMATCH = "AMOUNT_MISMATCH"
|
|
64
|
+
PROVIDER_UNCONFIRMED = "PROVIDER_UNCONFIRMED"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class CyrusAPIError(Exception):
|
|
5
|
+
"""Base exception for all Cyrus API errors."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, code: str, message: str, status_code: int | None = None) -> None:
|
|
8
|
+
self.code = code
|
|
9
|
+
self.message = message
|
|
10
|
+
self.status_code = status_code
|
|
11
|
+
super().__init__(message)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class NotFoundError(CyrusAPIError):
|
|
15
|
+
"""Resource not found (code 82)."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class UnauthorizedError(CyrusAPIError):
|
|
19
|
+
"""Invalid or missing API key (code 40)."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ValidationError(CyrusAPIError):
|
|
23
|
+
"""Request validation failed (code 70/71). Includes field-level errors."""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
code: str,
|
|
28
|
+
message: str,
|
|
29
|
+
field_errors: list[dict[str, str]] | None = None,
|
|
30
|
+
status_code: int | None = None,
|
|
31
|
+
) -> None:
|
|
32
|
+
self.field_errors = field_errors or []
|
|
33
|
+
super().__init__(code=code, message=message, status_code=status_code)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ConflictError(CyrusAPIError):
|
|
37
|
+
"""Resource state conflict (code 81, 409, etc.)."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ProviderError(CyrusAPIError):
|
|
41
|
+
"""Upstream provider (Nomba) error (code 90)."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ServerError(CyrusAPIError):
|
|
45
|
+
"""Internal server error (code 99, 5xx)."""
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from cyrus._exceptions import (
|
|
9
|
+
ConflictError,
|
|
10
|
+
CyrusAPIError,
|
|
11
|
+
NotFoundError,
|
|
12
|
+
ProviderError,
|
|
13
|
+
ServerError,
|
|
14
|
+
UnauthorizedError,
|
|
15
|
+
ValidationError,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
_DEFAULT_BASE_URL = "https://api.trycyrus.app"
|
|
19
|
+
_MAX_RETRIES = 3
|
|
20
|
+
_RETRY_BACKOFF = 0.5 # seconds, doubled each attempt
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class HttpClient:
|
|
24
|
+
"""Thin wrapper around httpx that handles auth, envelope parsing, and retries."""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
api_key: str,
|
|
29
|
+
*,
|
|
30
|
+
base_url: str = _DEFAULT_BASE_URL,
|
|
31
|
+
timeout: float = 30.0,
|
|
32
|
+
max_retries: int = _MAX_RETRIES,
|
|
33
|
+
) -> None:
|
|
34
|
+
self._client = httpx.Client(
|
|
35
|
+
base_url=base_url,
|
|
36
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
37
|
+
timeout=timeout,
|
|
38
|
+
)
|
|
39
|
+
self._max_retries = max_retries
|
|
40
|
+
|
|
41
|
+
def get(self, path: str, *, params: dict[str, Any] | None = None) -> Any:
|
|
42
|
+
return self._request("GET", path, params=params)
|
|
43
|
+
|
|
44
|
+
def post(self, path: str, *, json: dict[str, Any] | None = None) -> Any:
|
|
45
|
+
return self._request("POST", path, json=json)
|
|
46
|
+
|
|
47
|
+
def patch(self, path: str, *, json: dict[str, Any] | None = None) -> Any:
|
|
48
|
+
return self._request("PATCH", path, json=json)
|
|
49
|
+
|
|
50
|
+
def delete(self, path: str) -> Any:
|
|
51
|
+
return self._request("DELETE", path)
|
|
52
|
+
|
|
53
|
+
def _request(self, method: str, path: str, **kwargs: Any) -> Any:
|
|
54
|
+
last_exc: Exception | None = None
|
|
55
|
+
for attempt in range(self._max_retries):
|
|
56
|
+
try:
|
|
57
|
+
resp = self._client.request(method, path, **kwargs)
|
|
58
|
+
return self._handle_response(resp)
|
|
59
|
+
except (httpx.TimeoutException, httpx.ConnectError, httpx.RemoteProtocolError) as exc:
|
|
60
|
+
last_exc = exc
|
|
61
|
+
if attempt < self._max_retries - 1:
|
|
62
|
+
time.sleep(_RETRY_BACKOFF * (2 ** attempt))
|
|
63
|
+
raise CyrusAPIError(
|
|
64
|
+
code="99",
|
|
65
|
+
message=f"Connection failed after {self._max_retries} attempts: {last_exc}",
|
|
66
|
+
status_code=None,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def _handle_response(resp: httpx.Response) -> Any:
|
|
71
|
+
try:
|
|
72
|
+
body: dict[str, Any] = resp.json()
|
|
73
|
+
except ValueError:
|
|
74
|
+
# The api-key chain rejects missing/invalid keys before the request ever reaches a
|
|
75
|
+
# controller, so there's no CyrusApiResponse envelope to parse — just a bare status.
|
|
76
|
+
if resp.status_code in (401, 403):
|
|
77
|
+
raise UnauthorizedError(
|
|
78
|
+
code="40",
|
|
79
|
+
message="Invalid or missing API key",
|
|
80
|
+
status_code=resp.status_code,
|
|
81
|
+
) from None
|
|
82
|
+
raise CyrusAPIError(
|
|
83
|
+
code="99",
|
|
84
|
+
message=f"Unexpected non-JSON response (HTTP {resp.status_code})",
|
|
85
|
+
status_code=resp.status_code,
|
|
86
|
+
) from None
|
|
87
|
+
|
|
88
|
+
if resp.status_code >= 500:
|
|
89
|
+
msg = body.get("message", "Server error")
|
|
90
|
+
code = body.get("code", "99")
|
|
91
|
+
raise ServerError(code=code, message=msg, status_code=resp.status_code)
|
|
92
|
+
|
|
93
|
+
if body.get("status") is False or body.get("status") == "false":
|
|
94
|
+
code = body.get("code", "99")
|
|
95
|
+
msg = body.get("message", "Unknown error")
|
|
96
|
+
data = body.get("data") or {}
|
|
97
|
+
|
|
98
|
+
# Checked first: ResponseCode.INVALID_REQUEST ("71") is a generic business-rule-violation
|
|
99
|
+
# bucket the backend reuses across many exception types, several of which are genuine
|
|
100
|
+
# state conflicts returned with HTTP 409 (e.g. closing an already-closed customer,
|
|
101
|
+
# insufficient wallet funds). A 409 always means ConflictError regardless of which
|
|
102
|
+
# internal code produced it — checked before the code-based branches below so those
|
|
103
|
+
# conflicts aren't misclassified as ValidationError. Code "70" (INVALID_INPUT) is never
|
|
104
|
+
# emitted with 409 — it's exclusively bean-validation failures at 400 — so this reordering
|
|
105
|
+
# doesn't affect real field-validation errors.
|
|
106
|
+
if resp.status_code == 409 or code == "81":
|
|
107
|
+
raise ConflictError(code=code, message=msg, status_code=resp.status_code)
|
|
108
|
+
if code in ("70", "71"):
|
|
109
|
+
raise ValidationError(
|
|
110
|
+
code=code,
|
|
111
|
+
message=msg,
|
|
112
|
+
field_errors=data.get("fieldErrors") or [],
|
|
113
|
+
status_code=resp.status_code,
|
|
114
|
+
)
|
|
115
|
+
if code == "82":
|
|
116
|
+
raise NotFoundError(code=code, message=msg, status_code=resp.status_code)
|
|
117
|
+
if code == "40":
|
|
118
|
+
raise UnauthorizedError(code=code, message=msg, status_code=resp.status_code)
|
|
119
|
+
if code == "90":
|
|
120
|
+
raise ProviderError(code=code, message=msg, status_code=resp.status_code)
|
|
121
|
+
raise CyrusAPIError(code=code, message=msg, status_code=resp.status_code)
|
|
122
|
+
|
|
123
|
+
return body.get("data")
|
|
124
|
+
|
|
125
|
+
def close(self) -> None:
|
|
126
|
+
self._client.close()
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Iterator, TypeVar
|
|
4
|
+
|
|
5
|
+
from pydantic import TypeAdapter
|
|
6
|
+
|
|
7
|
+
from cyrus._http import HttpClient
|
|
8
|
+
from cyrus.models.responses import PageResponse
|
|
9
|
+
|
|
10
|
+
T = TypeVar("T")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Page:
|
|
14
|
+
"""A single page of results from a paginated endpoint."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, data: dict[str, Any], item_type: type[T]) -> None:
|
|
17
|
+
self._page = PageResponse.model_validate(data)
|
|
18
|
+
self._adapter = TypeAdapter(list[item_type]) # type: ignore[type-arg]
|
|
19
|
+
self.items: list[T] = self._adapter.validate_python(self._page.content)
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def meta(self) -> PageResponse:
|
|
23
|
+
return self._page
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def total_elements(self) -> int:
|
|
27
|
+
return self._page.total_elements
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def total_pages(self) -> int:
|
|
31
|
+
return self._page.total_pages
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def page_number(self) -> int:
|
|
35
|
+
return self._page.number
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def is_first(self) -> bool:
|
|
39
|
+
return self._page.first
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def is_last(self) -> bool:
|
|
43
|
+
return self._page.last
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def is_empty(self) -> bool:
|
|
47
|
+
return self._page.empty
|
|
48
|
+
|
|
49
|
+
def __iter__(self) -> Iterator[T]:
|
|
50
|
+
return iter(self.items)
|
|
51
|
+
|
|
52
|
+
def __getitem__(self, index: int) -> T:
|
|
53
|
+
return self.items[index]
|
|
54
|
+
|
|
55
|
+
def __len__(self) -> int:
|
|
56
|
+
return len(self.items)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class PageIterator:
|
|
60
|
+
"""Auto-paginating iterator that yields items across all pages."""
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
client: HttpClient,
|
|
65
|
+
path: str,
|
|
66
|
+
item_type: type[T],
|
|
67
|
+
*,
|
|
68
|
+
params: dict[str, Any] | None = None,
|
|
69
|
+
page_size: int = 20,
|
|
70
|
+
) -> None:
|
|
71
|
+
self._client = client
|
|
72
|
+
self._path = path
|
|
73
|
+
self._item_type = item_type
|
|
74
|
+
self._params = params or {}
|
|
75
|
+
self._page_size = page_size
|
|
76
|
+
self._current_page = 0
|
|
77
|
+
|
|
78
|
+
def __iter__(self) -> Iterator[T]:
|
|
79
|
+
while True:
|
|
80
|
+
params = {**self._params, "page": self._current_page, "size": self._page_size}
|
|
81
|
+
data = self._client.get(self._path, params=params)
|
|
82
|
+
page = Page(data, self._item_type)
|
|
83
|
+
yield from page.items
|
|
84
|
+
if page.is_last or page.is_empty:
|
|
85
|
+
break
|
|
86
|
+
self._current_page += 1
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def query_value(value: Any) -> Any:
|
|
7
|
+
"""Unwrap an Enum member to its wire value for use as a query param.
|
|
8
|
+
|
|
9
|
+
httpx's query-param encoder calls ``str()`` on non-primitive values, which for a
|
|
10
|
+
``(str, Enum)`` member yields ``"ClassName.MEMBER"`` rather than the member's value
|
|
11
|
+
(this only differs from JSON body encoding, which uses the member's str content directly).
|
|
12
|
+
Passing a plain string through unchanged keeps this backward-compatible with callers who
|
|
13
|
+
don't use the enum types.
|
|
14
|
+
"""
|
|
15
|
+
return getattr(value, "value", value)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from cyrus._http import HttpClient
|
|
4
|
+
from cyrus.resources.customers import Customers
|
|
5
|
+
from cyrus.resources.payment_events import PaymentEvents
|
|
6
|
+
from cyrus.resources.transactions import Transactions
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CyrusClient:
|
|
10
|
+
"""Client for the Cyrus Payments API.
|
|
11
|
+
|
|
12
|
+
Usage::
|
|
13
|
+
|
|
14
|
+
from cyrus import CyrusClient
|
|
15
|
+
|
|
16
|
+
client = CyrusClient(api_key="cyrus_live_xxx")
|
|
17
|
+
customer = client.customers.create(reference="cust_123", first_name="Ada")
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
api_key: str,
|
|
23
|
+
*,
|
|
24
|
+
base_url: str = "https://api.trycyrus.app",
|
|
25
|
+
timeout: float = 30.0,
|
|
26
|
+
max_retries: int = 3,
|
|
27
|
+
) -> None:
|
|
28
|
+
self._http = HttpClient(
|
|
29
|
+
api_key,
|
|
30
|
+
base_url=base_url,
|
|
31
|
+
timeout=timeout,
|
|
32
|
+
max_retries=max_retries,
|
|
33
|
+
)
|
|
34
|
+
self.customers = Customers(self._http)
|
|
35
|
+
self.transactions = Transactions(self._http)
|
|
36
|
+
self.payment_events = PaymentEvents(self._http)
|
|
37
|
+
|
|
38
|
+
def close(self) -> None:
|
|
39
|
+
self._http.close()
|
|
40
|
+
|
|
41
|
+
def __enter__(self) -> CyrusClient:
|
|
42
|
+
return self
|
|
43
|
+
|
|
44
|
+
def __exit__(self, *args: object) -> None:
|
|
45
|
+
self.close()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from cyrus.models.requests import (
|
|
2
|
+
CreateCustomer,
|
|
3
|
+
ReattributePaymentEvent,
|
|
4
|
+
UpdateCustomer,
|
|
5
|
+
UpdateCustomerStatus,
|
|
6
|
+
UpdateKycTier,
|
|
7
|
+
)
|
|
8
|
+
from cyrus.models.responses import (
|
|
9
|
+
Customer,
|
|
10
|
+
CustomerListItem,
|
|
11
|
+
CustomerStatement,
|
|
12
|
+
PageResponse,
|
|
13
|
+
PaymentEventDetail,
|
|
14
|
+
PaymentEventListItem,
|
|
15
|
+
ReattributionResult,
|
|
16
|
+
StatementRow,
|
|
17
|
+
StatementSummary,
|
|
18
|
+
Transaction,
|
|
19
|
+
VirtualAccountSummary,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"CreateCustomer",
|
|
24
|
+
"Customer",
|
|
25
|
+
"CustomerListItem",
|
|
26
|
+
"CustomerStatement",
|
|
27
|
+
"PageResponse",
|
|
28
|
+
"PaymentEventDetail",
|
|
29
|
+
"PaymentEventListItem",
|
|
30
|
+
"ReattributionResult",
|
|
31
|
+
"ReattributePaymentEvent",
|
|
32
|
+
"StatementRow",
|
|
33
|
+
"StatementSummary",
|
|
34
|
+
"Transaction",
|
|
35
|
+
"UpdateCustomer",
|
|
36
|
+
"UpdateCustomerStatus",
|
|
37
|
+
"UpdateKycTier",
|
|
38
|
+
"VirtualAccountSummary",
|
|
39
|
+
]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
4
|
+
|
|
5
|
+
from cyrus._enums import CustomerStatus, KycTier
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CreateCustomer(BaseModel):
|
|
9
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
10
|
+
|
|
11
|
+
reference: str
|
|
12
|
+
first_name: str = Field(alias="firstName")
|
|
13
|
+
last_name: str | None = Field(default=None, alias="lastName")
|
|
14
|
+
email: str | None = None
|
|
15
|
+
phone_number: str | None = Field(default=None, alias="phoneNumber")
|
|
16
|
+
bvn: str | None = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class UpdateCustomer(BaseModel):
|
|
20
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
21
|
+
|
|
22
|
+
first_name: str | None = Field(default=None, alias="firstName")
|
|
23
|
+
last_name: str | None = Field(default=None, alias="lastName")
|
|
24
|
+
email: str | None = None
|
|
25
|
+
phone_number: str | None = Field(default=None, alias="phoneNumber")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class UpdateCustomerStatus(BaseModel):
|
|
29
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
30
|
+
|
|
31
|
+
status: CustomerStatus
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class UpdateKycTier(BaseModel):
|
|
35
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
36
|
+
|
|
37
|
+
tier: KycTier
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ReattributePaymentEvent(BaseModel):
|
|
41
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
42
|
+
|
|
43
|
+
customer_reference: str = Field(alias="customerReference")
|