epoint-python-sdk 0.2.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.
@@ -0,0 +1,154 @@
1
+ Metadata-Version: 2.5
2
+ Name: epoint-python-sdk
3
+ Version: 0.2.0
4
+ Summary: Typed Python client for the Epoint (epoint.az) payment gateway, sync and async, safe against double charges.
5
+ Project-URL: Homepage, https://theqiyas.cc/projects/epoint-python-sdk/
6
+ Project-URL: Source, https://github.com/qiyascc/epoint-python-sdk
7
+ Project-URL: Author, https://theqiyas.cc
8
+ Project-URL: Issues, https://github.com/qiyascc/epoint-python-sdk/issues
9
+ Project-URL: Changelog, https://github.com/qiyascc/epoint-python-sdk/blob/main/CHANGELOG.md
10
+ Author-email: qiyascc <qiyasturk@gmail.com>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: azerbaijan,azn,checkout,epoint,gateway,payment,sdk
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Office/Business :: Financial
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.10
25
+ Requires-Dist: goodconf>=7.1
26
+ Requires-Dist: httpx<1,>=0.24
27
+ Requires-Dist: pydantic>=2.7
28
+ Requires-Dist: pyyaml>=6
29
+ Provides-Extra: dev
30
+ Requires-Dist: goodconf[toml]>=7.1; extra == 'dev'
31
+ Requires-Dist: goodconf[yaml]>=7.1; extra == 'dev'
32
+ Requires-Dist: mypy>=1.8; extra == 'dev'
33
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
34
+ Requires-Dist: pytest>=7; extra == 'dev'
35
+ Requires-Dist: ruff>=0.4; extra == 'dev'
36
+ Requires-Dist: types-pyyaml; extra == 'dev'
37
+ Provides-Extra: toml
38
+ Requires-Dist: goodconf[toml]>=7.1; extra == 'toml'
39
+ Provides-Extra: yaml
40
+ Requires-Dist: goodconf[yaml]>=7.1; extra == 'yaml'
41
+ Description-Content-Type: text/markdown
42
+
43
+ # epoint-python-sdk
44
+
45
+ [![Python](https://img.shields.io/badge/python-3.10%20to%203.13-3776AB?logo=python&logoColor=white)](https://www.python.org)
46
+ [![Epoint](https://img.shields.io/badge/gateway-epoint.az-0B5FFF)](https://epoint.az)
47
+ [![Typed](https://img.shields.io/badge/typing-mypy-2A6DB2)](https://mypy-lang.org)
48
+ [![Async](https://img.shields.io/badge/sync%20%2B%20async-httpx-5A45FF)](https://www.python-httpx.org)
49
+ [![Ruff](https://img.shields.io/badge/lint-ruff-D7FF64?logo=ruff&logoColor=black)](https://docs.astral.sh/ruff)
50
+ [![Apple Pay](https://img.shields.io/badge/Apple%20Pay-supported-000000?logo=applepay&logoColor=white)](#apple-pay-and-google-pay)
51
+ [![Google Pay](https://img.shields.io/badge/Google%20Pay-supported-4285F4?logo=googlepay&logoColor=white)](#apple-pay-and-google-pay)
52
+ [![License](https://img.shields.io/badge/license-MIT-green)](https://github.com/qiyascc/epoint-python-sdk/blob/main/LICENSE)
53
+
54
+ Typed Python client for the [Epoint](https://epoint.az) payment gateway. Sync and async, every endpoint, signed callbacks, and retry rules built so a payment is never sent twice.
55
+
56
+ ```bash
57
+ pip install epoint-python-sdk
58
+ ```
59
+
60
+ ## Pay
61
+
62
+ ```python
63
+ from epoint_sdk import Epoint
64
+
65
+ with Epoint("i000000001", "PRIVATE_KEY", success_url="https://shop.az/ok", error_url="https://shop.az/fail") as ep:
66
+ checkout = ep.payments.create("19.90", "ORDER-1042", description="Pro plan")
67
+ redirect(checkout.redirect_url)
68
+ ```
69
+
70
+ ## Confirm
71
+
72
+ ```python
73
+ tx = ep.verify_callback(request.POST)
74
+ if tx.paid:
75
+ fulfil(tx.order_id, tx.amount, tx.transaction)
76
+ ```
77
+
78
+ Never trust the redirect alone. Use the signed callback, or ask the gateway:
79
+
80
+ ```python
81
+ ep.payments.status(checkout.transaction).paid
82
+ ```
83
+
84
+ ## Apple Pay and Google Pay
85
+
86
+ ```python
87
+ widget = ep.widgets.create("19.90", "ORDER-1043", "Pro plan")
88
+ ```
89
+
90
+ ```html
91
+ <iframe src="{{ widget.widget_url }}" allow="payment" title="Apple Pay / Google Pay"></iframe>
92
+ <script>
93
+ addEventListener("message", (e) => {
94
+ if (e.origin !== "{{ widget.origin }}") return;
95
+ fetch("/orders/1043/check/", { method: "POST" });
96
+ });
97
+ </script>
98
+ ```
99
+
100
+ The page needs `Permissions-Policy: payment=(self "https://epoint.az")`. The `postMessage` is a hint only: `WidgetMessage.from_event(data)` reads it, then confirm on the server with `ep.payments.status(widget.transaction)` or the signed callback. Epoint enables token payments per merchant. `ep.widgets.split(...)` is the split variant.
101
+
102
+ ## Safety
103
+
104
+ | Situation | What the client does |
105
+ | --- | --- |
106
+ | Connection refused, nothing sent | Retries, then `NotSentError` |
107
+ | Timeout, cut connection or 5xx on a call that moves money | No retry, `OutcomeUnknownError` with the `order_id` |
108
+ | Same failure on a read (status, card, lists) | Retries with backoff |
109
+ | 429 | Retries, the gateway did not process it |
110
+ | `status: error` from the gateway | `GatewayError` with `code` and `trace_id` |
111
+ | Declined card on a direct charge | Returns `Transaction` with `failed`, never raises |
112
+ | Amount like `1.005`, `0`, `NaN` or `True` | `InvalidRequestError` before anything is sent |
113
+ | `base_url` over plain http | `ConfigurationError`, except localhost or `allow_insecure_http` |
114
+ | Callback `data` over 64 KB | Rejected before hashing |
115
+ | Callback for another order or amount | `tx.matches(order_id=..., amount=...)` is `False` |
116
+
117
+ Reuse the same `order_id` when you retry. Epoint refuses a used one, so a double click or a crash can not charge twice. Amounts are `Decimal` end to end, and signatures are compared in constant time.
118
+
119
+ ## Endpoints
120
+
121
+ | Namespace | Methods |
122
+ | --- | --- |
123
+ | `payments` | `create` `form` `status` `reverse` `hold` `capture` |
124
+ | `cards` | `register` `register_and_pay` `charge` `status` `payout` |
125
+ | `split` | `create` `charge` `register_and_pay` |
126
+ | `installments` | `plans` `create` |
127
+ | `wallets` | `list` `pay` |
128
+ | `widgets` | `create` `split` (Apple Pay, Google Pay) |
129
+ | `invoices` | `create` `update` `get` `list` `send_sms` `send_email` |
130
+ | `transfers` | `create` `status` (B2B) |
131
+ | client | `heartbeat` `verify_callback` `request` |
132
+
133
+ `AsyncEpoint` has the same methods, awaited.
134
+
135
+ ## Config
136
+
137
+ Pass values in code, or set `EPOINT_PUBLIC_KEY`, `EPOINT_PRIVATE_KEY`, `EPOINT_SUCCESS_URL`, `EPOINT_ERROR_URL`, `EPOINT_LANGUAGE`, `EPOINT_TIMEOUT`, `EPOINT_MAX_RETRIES`, or put them in `epoint.toml`. Every path is a setting (`EPOINT_ENDPOINTS__PAYMENT=...`) and `base_url` can point at a sandbox such as [epoint-sandbox](https://github.com/martian56/epoint-sandbox). Messages live in `messages.yaml` and can be replaced with `messages.use(...)`.
138
+
139
+ ## Develop
140
+
141
+ ```bash
142
+ pip install -e ".[dev]" && pytest && ruff check . && mypy src
143
+ ```
144
+
145
+ ## Links
146
+
147
+ - Source code: [github.com/qiyascc/epoint-python-sdk](https://github.com/qiyascc/epoint-python-sdk)
148
+ - PyPI: [pypi.org/project/epoint-python-sdk](https://pypi.org/project/epoint-python-sdk/)
149
+ - Project page: [theqiyas.cc/projects/epoint-python-sdk](https://theqiyas.cc/projects/epoint-python-sdk/)
150
+ - Author: [Qiyas Sadıqov](https://theqiyas.cc), more projects at [theqiyas.cc](https://theqiyas.cc)
151
+
152
+ ## License
153
+
154
+ MIT
@@ -0,0 +1,19 @@
1
+ epoint_sdk/__init__.py,sha256=WYFCCz55UnbNnEjfKIHHwmKXyNrnJVAcZ0uR-TtLVVU,1437
2
+ epoint_sdk/_core.py,sha256=AuaYPignhuMOFngYyNXGR3alE4GTU6zHVUDqlcX3yAY,8803
3
+ epoint_sdk/_version.py,sha256=Zn1KFblwuFHiDRdRAiRnDBRkbPttWh44jKa5zG2ov0E,22
4
+ epoint_sdk/callbacks.py,sha256=tAkpr98K_tnsCjQ943I0-aaoSt1ghCfnLgXrRUPXHBY,1021
5
+ epoint_sdk/client.py,sha256=MYDPrbxPT9h7sboDUpsd3T9qKCm9wsbuNykJLWAEi3M,2019
6
+ epoint_sdk/config.py,sha256=18Gcsdk3GGFVmbuKQDGWRDBGWwniNFO1ZHenLxA-pX0,5084
7
+ epoint_sdk/enums.py,sha256=RCCUEeUwTwKhxqxBah64-_fC84YvtcWc6n_17hKDl6A,1185
8
+ epoint_sdk/errors.py,sha256=2Xa9GwYhom4jDlS1XO69neubPlQEWVJbzalnYots30E,1863
9
+ epoint_sdk/messages.py,sha256=GqqTi5PvVlt2G-F6llTb3_gMTlRgVj4DHMUXZQzr7ZY,1601
10
+ epoint_sdk/messages.yaml,sha256=NnpfG5laHqoIn7I_9Ok5Qs-Rp23zuM15wZB48ArCY1I,2419
11
+ epoint_sdk/models.py,sha256=45ElUKtm3HeiYqdfgSifoWEZfQ_PhSqKhBdqBlnSr0Y,9262
12
+ epoint_sdk/money.py,sha256=sgmV7wT0Q_khLGY7pO--LAyJDPMwEdJXspuNsxSG03Y,1443
13
+ epoint_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ epoint_sdk/resources.py,sha256=IamTdeJTgjab5A3I8KOyc-S4YXG-RDzOJxC9LBnX9RU,19984
15
+ epoint_sdk/signing.py,sha256=ZHDgS9D5gjFhITB_GSoFq8pHGE43dNdZW8LjYVdT_R0,1552
16
+ epoint_python_sdk-0.2.0.dist-info/METADATA,sha256=eaNaIf7tbmeJ3E7RR0QULF6eJ8i1goxEH34drqFzrIU,6847
17
+ epoint_python_sdk-0.2.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
18
+ epoint_python_sdk-0.2.0.dist-info/licenses/LICENSE,sha256=ENVL-_Eh4n1SumZuLuTjO2K0OOL_eXh4pkeme35b02I,1064
19
+ epoint_python_sdk-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 qiyascc
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
epoint_sdk/__init__.py ADDED
@@ -0,0 +1,73 @@
1
+ from . import messages, signing
2
+ from ._version import __version__
3
+ from .callbacks import callback_form, verify_callback
4
+ from .client import AsyncEpoint, Epoint
5
+ from .config import Settings, load_settings
6
+ from .enums import CardStatus, Currency, Language, OperationCode, Status
7
+ from .errors import (
8
+ ConfigurationError,
9
+ EpointError,
10
+ GatewayError,
11
+ HTTPStatusError,
12
+ InvalidRequestError,
13
+ NotSentError,
14
+ OutcomeUnknownError,
15
+ SignatureError,
16
+ TransportError,
17
+ )
18
+ from .models import (
19
+ Callback,
20
+ Card,
21
+ CardRegistration,
22
+ CheckoutForm,
23
+ InstallmentPlan,
24
+ Invoice,
25
+ Redirect,
26
+ Result,
27
+ Transaction,
28
+ Transfer,
29
+ Wallet,
30
+ Widget,
31
+ WidgetMessage,
32
+ origin_of,
33
+ )
34
+
35
+ __all__ = [
36
+ "__version__",
37
+ "Epoint",
38
+ "AsyncEpoint",
39
+ "Settings",
40
+ "load_settings",
41
+ "messages",
42
+ "signing",
43
+ "verify_callback",
44
+ "callback_form",
45
+ "Status",
46
+ "CardStatus",
47
+ "Currency",
48
+ "Language",
49
+ "OperationCode",
50
+ "Callback",
51
+ "Card",
52
+ "CardRegistration",
53
+ "CheckoutForm",
54
+ "InstallmentPlan",
55
+ "Invoice",
56
+ "Redirect",
57
+ "Result",
58
+ "Transaction",
59
+ "Transfer",
60
+ "Wallet",
61
+ "Widget",
62
+ "WidgetMessage",
63
+ "origin_of",
64
+ "EpointError",
65
+ "ConfigurationError",
66
+ "InvalidRequestError",
67
+ "SignatureError",
68
+ "TransportError",
69
+ "NotSentError",
70
+ "OutcomeUnknownError",
71
+ "HTTPStatusError",
72
+ "GatewayError",
73
+ ]
epoint_sdk/_core.py ADDED
@@ -0,0 +1,209 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import random
5
+ import time
6
+ from dataclasses import dataclass, field
7
+ from typing import Any, Callable, Mapping, Optional, TypeVar
8
+
9
+ import httpx
10
+
11
+ from . import signing
12
+ from .config import Settings, load_settings
13
+ from .enums import Status
14
+ from .errors import GatewayError, HTTPStatusError, NotSentError, OutcomeUnknownError, TransportError
15
+ from .messages import text
16
+
17
+ T = TypeVar("T")
18
+
19
+ NOT_SENT = (httpx.ConnectError, httpx.ConnectTimeout, httpx.PoolTimeout, httpx.UnsupportedProtocol)
20
+ RETRY_STATUSES = frozenset({429, 500, 502, 503, 504})
21
+
22
+
23
+ @dataclass
24
+ class Op:
25
+ endpoint: str
26
+ payload: Optional[dict[str, Any]] = None
27
+ parse: Callable[[Any], Any] = lambda body: body
28
+ safe: bool = False
29
+ method: str = "POST"
30
+ signed: bool = True
31
+ json_body: bool = False
32
+ path_params: Mapping[str, Any] = field(default_factory=dict)
33
+ payment_state: bool = False
34
+ order_id: Optional[str] = None
35
+
36
+
37
+ def clean(payload: Mapping[str, Any]) -> dict[str, Any]:
38
+ return {key: value for key, value in payload.items() if value is not None and value != ""}
39
+
40
+
41
+ class Outcome:
42
+ def __init__(self, value: Any = None, error: Optional[Exception] = None, retry: bool = False) -> None:
43
+ self.value = value
44
+ self.error = error
45
+ self.retry = retry
46
+
47
+
48
+ class Engine:
49
+ def __init__(
50
+ self,
51
+ public_key: Optional[str] = None,
52
+ private_key: Optional[str] = None,
53
+ *,
54
+ settings: Optional[Settings] = None,
55
+ config_file: Optional[str] = None,
56
+ **overrides: Any,
57
+ ) -> None:
58
+ if settings is None:
59
+ settings = load_settings(config_file, public_key=public_key, private_key=private_key, **overrides)
60
+ elif public_key or private_key or overrides:
61
+ changes = {k: v for k, v in dict(overrides, public_key=public_key, private_key=private_key).items()
62
+ if v is not None}
63
+ current = settings.model_dump()
64
+ current["private_key"] = settings.private_key.get_secret_value()
65
+ settings = load_settings(None, **{**current, **changes})
66
+ self.settings = settings
67
+ settings.check_transport()
68
+ self.public_key, self._private_key = settings.keys()
69
+
70
+ def __repr__(self) -> str:
71
+ return f"{type(self).__name__}(public_key={self.public_key!r}, base_url={self.settings.base_url!r})"
72
+
73
+ def _timeout(self) -> httpx.Timeout:
74
+ return httpx.Timeout(self.settings.timeout, connect=self.settings.connect_timeout)
75
+
76
+ def _headers(self) -> dict[str, str]:
77
+ return {"Accept": "application/json", "User-Agent": self.settings.resolved_user_agent()}
78
+
79
+ def sign(self, payload: Mapping[str, Any]) -> dict[str, str]:
80
+ body = {"public_key": self.public_key, **clean(payload)}
81
+ data = signing.encode(body)
82
+ return {"data": data, "signature": signing.sign(self._private_key, data)}
83
+
84
+ def _request_args(self, op: Op) -> dict[str, Any]:
85
+ args: dict[str, Any] = {"headers": self._headers()}
86
+ if op.method == "POST":
87
+ body = self.sign(op.payload or {}) if op.signed else clean(op.payload or {})
88
+ args["json" if op.json_body else "data"] = body
89
+ return args
90
+
91
+ def _wait(self, attempt: int) -> float:
92
+ base = self.settings.backoff_base
93
+ return min(self.settings.backoff_cap, base * (2 ** attempt)) + random.uniform(0, base / 2)
94
+
95
+ def _on_exception(self, op: Op, exc: Exception, attempt: int) -> Outcome:
96
+ path = self.settings.url(op.endpoint, **op.path_params)
97
+ if isinstance(exc, NOT_SENT):
98
+ if attempt < self.settings.max_retries:
99
+ return Outcome(retry=True)
100
+ return Outcome(error=NotSentError(text("error.connect", error=exc), path=path, order_id=op.order_id))
101
+ if op.safe:
102
+ if attempt < self.settings.max_retries:
103
+ return Outcome(retry=True)
104
+ return Outcome(error=TransportError(text("error.timeout", error=exc), path=path, order_id=op.order_id))
105
+ return Outcome(error=OutcomeUnknownError(text("error.unknown_outcome", reason=type(exc).__name__), path=path,
106
+ order_id=op.order_id))
107
+
108
+ def _on_response(self, op: Op, response: httpx.Response, attempt: int) -> Outcome:
109
+ path = self.settings.url(op.endpoint, **op.path_params)
110
+ status = response.status_code
111
+ if status in RETRY_STATUSES and (op.safe or status == 429) and attempt < self.settings.max_retries:
112
+ return Outcome(retry=True)
113
+ if status >= 500 and not op.safe:
114
+ return Outcome(error=OutcomeUnknownError(text("error.unknown_outcome", reason=f"HTTP {status}"),
115
+ status_code=status, path=path, order_id=op.order_id))
116
+ try:
117
+ body = response.json()
118
+ except ValueError:
119
+ snippet = response.text[:200]
120
+ if status < 400 and not op.safe:
121
+ return Outcome(error=OutcomeUnknownError(text("error.body", body=snippet), status_code=status,
122
+ path=path, order_id=op.order_id))
123
+ return Outcome(error=HTTPStatusError(text("error.body", body=snippet), status_code=status, body=snippet,
124
+ path=path))
125
+ if isinstance(body, dict):
126
+ refused = Status.of(body.get("status")) is Status.ERROR
127
+ if refused and not (op.payment_state and body.get("transaction")):
128
+ message = str(body.get("message") or body.get("detail") or text("error.refused"))
129
+ code = body.get("code")
130
+ return Outcome(error=GatewayError(message, code=str(code) if code not in (None, "") else None,
131
+ trace_id=body.get("trace_id"), status_code=status, payload=body,
132
+ path=path))
133
+ if status >= 400:
134
+ detail = body.get("detail") or body.get("message") if isinstance(body, dict) else None
135
+ return Outcome(error=HTTPStatusError(str(detail or text("error.status", status=status)), status_code=status,
136
+ body=body, path=path))
137
+ return Outcome(value=op.parse(body))
138
+
139
+
140
+ class SyncEngine(Engine):
141
+ def __init__(self, *args: Any, http_client: Optional[httpx.Client] = None, **kwargs: Any) -> None:
142
+ super().__init__(*args, **kwargs)
143
+ self._owns_http = http_client is None
144
+ self._http = http_client or httpx.Client(timeout=self._timeout())
145
+
146
+ def close(self) -> None:
147
+ if self._owns_http:
148
+ self._http.close()
149
+
150
+ def __enter__(self: T) -> T:
151
+ return self
152
+
153
+ def __exit__(self, *exc: Any) -> None:
154
+ self.close()
155
+
156
+ def _run(self, op: Op) -> Any:
157
+ url = self.settings.url(op.endpoint, **op.path_params)
158
+ args = self._request_args(op)
159
+ attempt = 0
160
+ while True:
161
+ try:
162
+ response = self._http.request(op.method, url, **args)
163
+ except httpx.HTTPError as exc:
164
+ outcome = self._on_exception(op, exc, attempt)
165
+ else:
166
+ outcome = self._on_response(op, response, attempt)
167
+ if outcome.retry:
168
+ time.sleep(self._wait(attempt))
169
+ attempt += 1
170
+ continue
171
+ if outcome.error is not None:
172
+ raise outcome.error
173
+ return outcome.value
174
+
175
+
176
+ class AsyncEngine(Engine):
177
+ def __init__(self, *args: Any, http_client: Optional[httpx.AsyncClient] = None, **kwargs: Any) -> None:
178
+ super().__init__(*args, **kwargs)
179
+ self._owns_http = http_client is None
180
+ self._http = http_client or httpx.AsyncClient(timeout=self._timeout())
181
+
182
+ async def close(self) -> None:
183
+ if self._owns_http:
184
+ await self._http.aclose()
185
+
186
+ async def __aenter__(self: T) -> T:
187
+ return self
188
+
189
+ async def __aexit__(self, *exc: Any) -> None:
190
+ await self.close()
191
+
192
+ async def _run(self, op: Op) -> Any:
193
+ url = self.settings.url(op.endpoint, **op.path_params)
194
+ args = self._request_args(op)
195
+ attempt = 0
196
+ while True:
197
+ try:
198
+ response = await self._http.request(op.method, url, **args)
199
+ except httpx.HTTPError as exc:
200
+ outcome = self._on_exception(op, exc, attempt)
201
+ else:
202
+ outcome = self._on_response(op, response, attempt)
203
+ if outcome.retry:
204
+ await asyncio.sleep(self._wait(attempt))
205
+ attempt += 1
206
+ continue
207
+ if outcome.error is not None:
208
+ raise outcome.error
209
+ return outcome.value
epoint_sdk/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.2.0"
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Mapping, Union
4
+
5
+ from . import signing
6
+ from .errors import SignatureError
7
+ from .messages import text
8
+ from .models import Transaction
9
+
10
+ Form = Mapping[str, Any]
11
+
12
+
13
+ def _field(form: Form, key: str) -> str:
14
+ value = form.get(key)
15
+ if isinstance(value, (list, tuple)):
16
+ value = value[0] if value else ""
17
+ return str(value or "").strip()
18
+
19
+
20
+ def verify_callback(private_key: str, data: Union[str, Form], signature: str = "") -> Transaction:
21
+ raw = data if isinstance(data, str) else _field(data, "data")
22
+ if not isinstance(data, str):
23
+ signature = _field(data, "signature")
24
+ if not raw or not signature:
25
+ raise SignatureError(text("error.callback_fields"))
26
+ return Transaction.from_api(signing.verify(private_key, raw, signature))
27
+
28
+
29
+ def callback_form(private_key: str, payload: Mapping[str, Any]) -> dict[str, str]:
30
+ data = signing.encode(dict(payload))
31
+ return {"data": data, "signature": signing.sign(private_key, data)}
epoint_sdk/client.py ADDED
@@ -0,0 +1,55 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Mapping, Optional, Union
4
+
5
+ from ._core import AsyncEngine, Op, SyncEngine
6
+ from .callbacks import verify_callback
7
+ from .models import Transaction
8
+ from .resources import Cards, Installments, Invoices, Payments, Split, Transfers, Wallets, Widgets
9
+
10
+
11
+ def _alive(body: Any) -> bool:
12
+ return isinstance(body, Mapping) and str(body.get("status", "")).lower() == "ok"
13
+
14
+
15
+ class _Namespaces:
16
+ _private_key: str
17
+
18
+ def _mount(self) -> None:
19
+ self.payments = Payments(self)
20
+ self.cards = Cards(self)
21
+ self.split = Split(self)
22
+ self.installments = Installments(self)
23
+ self.wallets = Wallets(self)
24
+ self.widgets = Widgets(self)
25
+ self.invoices = Invoices(self)
26
+ self.transfers = Transfers(self)
27
+
28
+ def verify_callback(self, data: Union[str, Mapping[str, Any]], signature: str = "") -> Transaction:
29
+ return verify_callback(self._private_key, data, signature)
30
+
31
+
32
+ class Epoint(SyncEngine, _Namespaces):
33
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
34
+ super().__init__(*args, **kwargs)
35
+ self._mount()
36
+
37
+ def heartbeat(self) -> bool:
38
+ result: bool = self._run(Op("heartbeat", None, _alive, safe=True, method="GET", signed=False))
39
+ return result
40
+
41
+ def request(self, endpoint: str, payload: Optional[Mapping[str, Any]] = None, *, safe: bool = False) -> Any:
42
+ return self._run(Op(endpoint, dict(payload or {}), safe=safe))
43
+
44
+
45
+ class AsyncEpoint(AsyncEngine, _Namespaces):
46
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
47
+ super().__init__(*args, **kwargs)
48
+ self._mount()
49
+
50
+ async def heartbeat(self) -> bool:
51
+ result: bool = await self._run(Op("heartbeat", None, _alive, safe=True, method="GET", signed=False))
52
+ return result
53
+
54
+ async def request(self, endpoint: str, payload: Optional[Mapping[str, Any]] = None, *, safe: bool = False) -> Any:
55
+ return await self._run(Op(endpoint, dict(payload or {}), safe=safe))