heleket-sdk 0.3.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.
@@ -0,0 +1,48 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format is based on
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
5
+ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [0.3.0] - 2026-06-25
8
+
9
+ ### Added
10
+ - `HeleketPayout.refund(request)` → `POST /v1/payment/refund`. Refunds are now
11
+ signed with the **payout** API key, so the method lives on `HeleketPayout`.
12
+
13
+ ### Removed
14
+ - `HeleketPayment.refund()`. The Heleket API signs `/v1/payment/refund` with the
15
+ payout key, which a payment client does not hold, so it can no longer sign a
16
+ valid refund. Use `HeleketPayout.refund()` instead — e.g.
17
+ `HeleketPayout(merchant_id=merchant_id, api_key=payout_key).refund(request)`.
18
+ See [`UPGRADING.md`](UPGRADING.md).
19
+
20
+ ## [0.2.0] - 2026-06-25
21
+
22
+ ### Added
23
+ - `HeleketPayment.get_aml_links(uuid=None, order_id=None)` → `POST
24
+ /v1/payment/aml-links`: AML/KYC/SoF questionnaire links for a blocked (locked)
25
+ payment, plus `AmlLinkStatus` with `is_final()` / `is_successful()` helpers.
26
+
27
+ ### Changed
28
+ - `HeleketPayment.get_exchange_rates()` now issues a `GET` request (was `POST`).
29
+ The method signature and return type are unchanged.
30
+
31
+ ## [0.1.0]
32
+
33
+ Initial reference implementation of the Heleket cryptocurrency payment API.
34
+
35
+ ### Added
36
+ - `HeleketPayment` / `HeleketPayout` clients with typed Pydantic request/response
37
+ models.
38
+ - Payments API: invoices, info, history, static wallets, QR codes, refunds,
39
+ webhook resend, test webhooks.
40
+ - Payouts API: payouts, info, history, fee calculation, transfers, services.
41
+ - Balance and exchange-rate endpoints.
42
+ - Signed webhook verification (`WebhookVerifier`, `WebhookPayload`) with
43
+ constant-time comparison.
44
+ - Swappable HTTP transport (`Transport`, `RequestsTransport`, plus `FakeTransport`
45
+ for offline tests).
46
+ - Debug mode (`DebugSink`) and a webhook inspector CLI (`heleket-webhook-inspect`).
47
+ - Status enums (`PaymentStatus`, `PayoutStatus`) with `is_final()` /
48
+ `is_successful()` helpers.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Heleket
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.
@@ -0,0 +1,128 @@
1
+ Metadata-Version: 2.4
2
+ Name: heleket-sdk
3
+ Version: 0.3.0
4
+ Summary: Reference Python SDK for the Heleket cryptocurrency payment API: payments, payouts, balance, exchange rates, and signed webhooks.
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Keywords: heleket,crypto,cryptocurrency,payments,payouts,sdk,api,webhooks
8
+ Author: Heleket Reference SDK Authors
9
+ Requires-Python: >=3.11,<4.0
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Office/Business :: Financial
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Classifier: Typing :: Typed
22
+ Requires-Dist: pydantic (>=2.5,<3.0)
23
+ Requires-Dist: requests (>=2.31,<3.0)
24
+ Project-URL: Bug Tracker, https://github.com/Heleket/python-sdk/issues
25
+ Project-URL: Changelog, https://github.com/Heleket/python-sdk/blob/main/CHANGELOG.md
26
+ Project-URL: Documentation, https://doc.heleket.com/
27
+ Project-URL: Homepage, https://heleket.com
28
+ Project-URL: Repository, https://github.com/Heleket/python-sdk
29
+ Description-Content-Type: text/markdown
30
+
31
+ # Heleket Python integration (reference)
32
+
33
+ A production-grade reference SDK for the [Heleket](https://heleket.com) cryptocurrency payment API. Written for **Python 3.11+**, published as `heleket-sdk` — works with FastAPI, Django, Flask, or plain scripts.
34
+
35
+ Covers the full documented surface (payments, payouts, balance, services, exchange rates), ships with pytest tests, runnable examples, byte-fidelity webhook verification, debug mode wired into your choice of logger, a `heleket-webhook-inspect` CLI, and a Docker harness.
36
+
37
+ Runtime dependencies: **Pydantic v2** (typed DTOs) and **requests** (HTTP). Everything else comes from the standard library.
38
+
39
+ ## Quickstart
40
+
41
+ ```python
42
+ from heleket_sdk import HeleketPayment
43
+ from heleket_sdk.types import CreateInvoiceRequest
44
+
45
+ client = HeleketPayment(merchant_id=merchant_id, api_key=payment_key)
46
+
47
+ invoice = client.create_invoice(
48
+ CreateInvoiceRequest(
49
+ amount="15.00",
50
+ currency="USD",
51
+ order_id="order-42",
52
+ lifetime=3600,
53
+ )
54
+ )
55
+
56
+ print(invoice.url) # → https://pay.heleket.com/pay/<uuid>
57
+ ```
58
+
59
+ ## Install
60
+
61
+ ```bash
62
+ poetry add heleket-sdk
63
+ # or
64
+ pip install heleket-sdk
65
+ ```
66
+
67
+ Requirements: Python 3.11+ (StrEnum, Self type, better type hints). Tested with CPython 3.11 and 3.12.
68
+
69
+ ## Documentation
70
+
71
+ Full reference lives in [`docs/`](docs/README.md):
72
+
73
+ - [01 — Installation](docs/01-installation.md)
74
+ - [02 — Configuration](docs/02-configuration.md)
75
+ - [03 — Architecture](docs/03-architecture.md)
76
+ - [04 — Payments API](docs/04-payments.md)
77
+ - [05 — Payouts API](docs/05-payouts.md)
78
+ - [06 — Webhooks](docs/06-webhooks.md) ⚑ critical reading
79
+ - [07 — Debugging](docs/07-debugging.md)
80
+ - [08 — Testing](docs/08-testing.md)
81
+ - [09 — Error handling](docs/09-error-handling.md)
82
+ - [10 — Reference (statuses, currencies, endpoints)](docs/10-reference.md)
83
+ - [12 — Troubleshooting](docs/12-troubleshooting.md)
84
+
85
+ ## What's in the box
86
+
87
+ ```
88
+ src/heleket_sdk/ Source — single source of truth
89
+ src/heleket_sdk/webhook Webhook signature verifier (byte-fidelity)
90
+ tests/ pytest suite + FakeTransport for offline testing
91
+ examples/ Twelve runnable scripts covering every endpoint
92
+ bin/ heleket-webhook-inspect — CLI for verifying webhook payloads
93
+ docker/ Multi-stage Dockerfile (python:3.11-slim)
94
+ docs/ Full module documentation
95
+ CHANGELOG.md Notable changes per release
96
+ UPGRADING.md Migration notes between versions
97
+ pyproject.toml Poetry build + ruff + mypy + pytest config
98
+ ```
99
+
100
+ ## Common tasks
101
+
102
+ ```bash
103
+ make install # poetry install --with dev
104
+ make test # pytest -q
105
+ make lint # ruff check
106
+ make fmt # ruff format
107
+ make fmt-check # ruff format --check
108
+ make typecheck # mypy in strict mode
109
+ make qa # All quality gates
110
+ make example-invoice # Create a real invoice (needs .env)
111
+ make example-webhook # Run the webhook listener on :8000
112
+ make webhook-inspect KEY=$K FILE=evt.json # Inspect a captured webhook
113
+ make docker-shell # Drop into a containerized Python 3.11 shell
114
+ make help # Full target list
115
+ ```
116
+
117
+ ## Security notes (read this)
118
+
119
+ - **Use `verify_raw` for production webhooks.** `verify(decoded)` is convenience-only and lossy — it can't recover the PHP-style `\/` slash escapes that Heleket's server sends. See [docs/06-webhooks.md](docs/06-webhooks.md).
120
+ - **De-duplicate replays.** Use a `(uuid, status)` key in your DB before doing side-effect work. SQLAlchemy and redis-py examples in [docs/06-webhooks.md](docs/06-webhooks.md#idempotency-and-replay-protection).
121
+ - **Whitelist Heleket's webhook source IP `31.133.220.8`** at your reverse proxy or firewall.
122
+ - **Two separate API keys** — payments and payouts. Mixing them breaks webhook verification. (One exception: `/v1/payment/refund` uses the **payout** key — call `HeleketPayout.refund()`.)
123
+ - **The SDK never logs API keys.** Debug-mode entries include URL, method, body, and status — but the `sign` header and API key are explicitly excluded.
124
+
125
+ ## License
126
+
127
+ MIT.
128
+
@@ -0,0 +1,97 @@
1
+ # Heleket Python integration (reference)
2
+
3
+ A production-grade reference SDK for the [Heleket](https://heleket.com) cryptocurrency payment API. Written for **Python 3.11+**, published as `heleket-sdk` — works with FastAPI, Django, Flask, or plain scripts.
4
+
5
+ Covers the full documented surface (payments, payouts, balance, services, exchange rates), ships with pytest tests, runnable examples, byte-fidelity webhook verification, debug mode wired into your choice of logger, a `heleket-webhook-inspect` CLI, and a Docker harness.
6
+
7
+ Runtime dependencies: **Pydantic v2** (typed DTOs) and **requests** (HTTP). Everything else comes from the standard library.
8
+
9
+ ## Quickstart
10
+
11
+ ```python
12
+ from heleket_sdk import HeleketPayment
13
+ from heleket_sdk.types import CreateInvoiceRequest
14
+
15
+ client = HeleketPayment(merchant_id=merchant_id, api_key=payment_key)
16
+
17
+ invoice = client.create_invoice(
18
+ CreateInvoiceRequest(
19
+ amount="15.00",
20
+ currency="USD",
21
+ order_id="order-42",
22
+ lifetime=3600,
23
+ )
24
+ )
25
+
26
+ print(invoice.url) # → https://pay.heleket.com/pay/<uuid>
27
+ ```
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ poetry add heleket-sdk
33
+ # or
34
+ pip install heleket-sdk
35
+ ```
36
+
37
+ Requirements: Python 3.11+ (StrEnum, Self type, better type hints). Tested with CPython 3.11 and 3.12.
38
+
39
+ ## Documentation
40
+
41
+ Full reference lives in [`docs/`](docs/README.md):
42
+
43
+ - [01 — Installation](docs/01-installation.md)
44
+ - [02 — Configuration](docs/02-configuration.md)
45
+ - [03 — Architecture](docs/03-architecture.md)
46
+ - [04 — Payments API](docs/04-payments.md)
47
+ - [05 — Payouts API](docs/05-payouts.md)
48
+ - [06 — Webhooks](docs/06-webhooks.md) ⚑ critical reading
49
+ - [07 — Debugging](docs/07-debugging.md)
50
+ - [08 — Testing](docs/08-testing.md)
51
+ - [09 — Error handling](docs/09-error-handling.md)
52
+ - [10 — Reference (statuses, currencies, endpoints)](docs/10-reference.md)
53
+ - [12 — Troubleshooting](docs/12-troubleshooting.md)
54
+
55
+ ## What's in the box
56
+
57
+ ```
58
+ src/heleket_sdk/ Source — single source of truth
59
+ src/heleket_sdk/webhook Webhook signature verifier (byte-fidelity)
60
+ tests/ pytest suite + FakeTransport for offline testing
61
+ examples/ Twelve runnable scripts covering every endpoint
62
+ bin/ heleket-webhook-inspect — CLI for verifying webhook payloads
63
+ docker/ Multi-stage Dockerfile (python:3.11-slim)
64
+ docs/ Full module documentation
65
+ CHANGELOG.md Notable changes per release
66
+ UPGRADING.md Migration notes between versions
67
+ pyproject.toml Poetry build + ruff + mypy + pytest config
68
+ ```
69
+
70
+ ## Common tasks
71
+
72
+ ```bash
73
+ make install # poetry install --with dev
74
+ make test # pytest -q
75
+ make lint # ruff check
76
+ make fmt # ruff format
77
+ make fmt-check # ruff format --check
78
+ make typecheck # mypy in strict mode
79
+ make qa # All quality gates
80
+ make example-invoice # Create a real invoice (needs .env)
81
+ make example-webhook # Run the webhook listener on :8000
82
+ make webhook-inspect KEY=$K FILE=evt.json # Inspect a captured webhook
83
+ make docker-shell # Drop into a containerized Python 3.11 shell
84
+ make help # Full target list
85
+ ```
86
+
87
+ ## Security notes (read this)
88
+
89
+ - **Use `verify_raw` for production webhooks.** `verify(decoded)` is convenience-only and lossy — it can't recover the PHP-style `\/` slash escapes that Heleket's server sends. See [docs/06-webhooks.md](docs/06-webhooks.md).
90
+ - **De-duplicate replays.** Use a `(uuid, status)` key in your DB before doing side-effect work. SQLAlchemy and redis-py examples in [docs/06-webhooks.md](docs/06-webhooks.md#idempotency-and-replay-protection).
91
+ - **Whitelist Heleket's webhook source IP `31.133.220.8`** at your reverse proxy or firewall.
92
+ - **Two separate API keys** — payments and payouts. Mixing them breaks webhook verification. (One exception: `/v1/payment/refund` uses the **payout** key — call `HeleketPayout.refund()`.)
93
+ - **The SDK never logs API keys.** Debug-mode entries include URL, method, body, and status — but the `sign` header and API key are explicitly excluded.
94
+
95
+ ## License
96
+
97
+ MIT.
@@ -0,0 +1,36 @@
1
+ # Upgrading
2
+
3
+ ## 0.2 → 0.3
4
+
5
+ `refund` moved from `HeleketPayment` to `HeleketPayout`. The Heleket API now signs
6
+ `POST /v1/payment/refund` with the **payout** API key, so a payment client can no
7
+ longer produce a valid signature for it.
8
+
9
+ `HeleketPayment` no longer exposes `refund()` — calling it raises `AttributeError`.
10
+ Switch to `HeleketPayout.refund()`:
11
+
12
+ ```python
13
+ from heleket_sdk import HeleketPayment, HeleketPayout
14
+ from heleket_sdk.types import RefundRequest
15
+
16
+ # Before (0.2 and earlier)
17
+ payment = HeleketPayment(merchant_id=merchant_id, api_key=payment_key)
18
+ payment.refund(RefundRequest(uuid=invoice_uuid, address=addr, is_subtract=True))
19
+
20
+ # After (0.3)
21
+ payout = HeleketPayout(merchant_id=merchant_id, api_key=payout_key)
22
+ payout.refund(RefundRequest(uuid=invoice_uuid, address=addr, is_subtract=True))
23
+ ```
24
+
25
+ Everything else is unchanged.
26
+
27
+ ## 0.1 → 0.2
28
+
29
+ Purely additive — no caller changes required:
30
+
31
+ - New `HeleketPayment.get_aml_links(uuid=None, order_id=None)` returns the
32
+ AML/KYC/SoF questionnaire links for a blocked (locked) payment, alongside a new
33
+ `AmlLinkStatus` enum with `is_final()` / `is_successful()` helpers.
34
+ - `HeleketPayment.get_exchange_rates()` now issues a `GET` request instead of a
35
+ `POST`. The method signature and return type are identical, so existing code
36
+ keeps working.
@@ -0,0 +1,97 @@
1
+ [build-system]
2
+ requires = ["poetry-core>=1.6.0"]
3
+ build-backend = "poetry.core.masonry.api"
4
+
5
+ [tool.poetry]
6
+ name = "heleket-sdk"
7
+ version = "0.3.0"
8
+ description = "Reference Python SDK for the Heleket cryptocurrency payment API: payments, payouts, balance, exchange rates, and signed webhooks."
9
+ authors = ["Heleket Reference SDK Authors"]
10
+ license = "MIT"
11
+ readme = "README.md"
12
+ homepage = "https://heleket.com"
13
+ repository = "https://github.com/Heleket/python-sdk"
14
+ documentation = "https://doc.heleket.com/"
15
+ keywords = ["heleket", "crypto", "cryptocurrency", "payments", "payouts", "sdk", "api", "webhooks"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Intended Audience :: Developers",
19
+ "Topic :: Software Development :: Libraries :: Python Modules",
20
+ "Topic :: Office/Business :: Financial",
21
+ "Operating System :: OS Independent",
22
+ "Typing :: Typed",
23
+ ]
24
+ packages = [{ include = "heleket_sdk", from = "src" }]
25
+ include = [
26
+ { path = "CHANGELOG.md", format = "sdist" },
27
+ { path = "UPGRADING.md", format = "sdist" },
28
+ ]
29
+
30
+ [tool.poetry.urls]
31
+ Changelog = "https://github.com/Heleket/python-sdk/blob/main/CHANGELOG.md"
32
+ "Bug Tracker" = "https://github.com/Heleket/python-sdk/issues"
33
+
34
+ [tool.poetry.dependencies]
35
+ python = "^3.11"
36
+ pydantic = "^2.5"
37
+ requests = "^2.31"
38
+
39
+ [tool.poetry.group.dev.dependencies]
40
+ pytest = "^8.0"
41
+ pytest-cov = "^5.0"
42
+ ruff = "^0.5"
43
+ mypy = "^1.10"
44
+ types-requests = "^2.31"
45
+
46
+ [tool.poetry.scripts]
47
+ heleket-webhook-inspect = "heleket_sdk._cli:main"
48
+
49
+ # ---------- ruff ----------------------------------------------------------------
50
+
51
+ [tool.ruff]
52
+ line-length = 100
53
+ target-version = "py311"
54
+ src = ["src", "tests", "examples", "bin"]
55
+
56
+ [tool.ruff.lint]
57
+ select = [
58
+ "E", # pycodestyle errors
59
+ "W", # pycodestyle warnings
60
+ "F", # pyflakes
61
+ "I", # isort
62
+ "B", # bugbear
63
+ "UP", # pyupgrade
64
+ "PL", # pylint subset
65
+ "SIM", # simplifications
66
+ ]
67
+ ignore = [
68
+ "PLR0913", # too many arguments — DTO constructors are intentionally wide
69
+ "PLR2004", # magic numbers — fine in tests
70
+ ]
71
+
72
+ [tool.ruff.lint.per-file-ignores]
73
+ "tests/*" = ["PLR2004", "S101"]
74
+ "examples/*" = ["T201"] # print() is the point in examples
75
+
76
+ [tool.ruff.format]
77
+ quote-style = "double"
78
+
79
+ # ---------- mypy ----------------------------------------------------------------
80
+
81
+ [tool.mypy]
82
+ python_version = "3.11"
83
+ strict = true
84
+ plugins = ["pydantic.mypy"]
85
+ files = ["src/heleket_sdk"]
86
+
87
+ [tool.pydantic-mypy]
88
+ init_forbid_extra = false
89
+ init_typed = true
90
+ warn_required_dynamic_aliases = true
91
+
92
+ # ---------- pytest --------------------------------------------------------------
93
+
94
+ [tool.pytest.ini_options]
95
+ minversion = "8.0"
96
+ testpaths = ["tests"]
97
+ addopts = "-ra --strict-markers --strict-config"
@@ -0,0 +1,72 @@
1
+ """Heleket Python SDK.
2
+
3
+ A reference implementation of the `Heleket <https://heleket.com>`_
4
+ cryptocurrency payment API. Covers payments, payouts, balance, services,
5
+ exchange rates, and webhook verification.
6
+
7
+ Public entry points::
8
+
9
+ from heleket_sdk import HeleketPayment, HeleketPayout
10
+ from heleket_sdk import ClientOptions
11
+ from heleket_sdk import WebhookVerifier
12
+ from heleket_sdk import PaymentStatus, PayoutStatus
13
+ from heleket_sdk import HeleketError, ApiError, ValidationError, HttpError, SignatureError
14
+
15
+ See ``docs/`` for the full reference.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from ._config import DEFAULT_BASE_URL, DEFAULT_TIMEOUT_SECONDS, ClientOptions
21
+ from ._debug import DebugEntry, DebugSink, stderr_sink
22
+ from ._errors import (
23
+ ApiError,
24
+ HeleketError,
25
+ HttpError,
26
+ SignatureError,
27
+ ValidationError,
28
+ )
29
+ from ._signature import constant_time_equal, sign
30
+ from ._status import AmlLinkStatus, CourseSource, PaymentStatus, PayoutStatus
31
+ from ._transport import RequestsTransport, Transport, TransportResponse
32
+ from ._version import __version__
33
+ from .payment import HeleketPayment
34
+ from .payout import HeleketPayout
35
+ from .webhook import WebhookPayload, WebhookVerifier
36
+
37
+ __all__ = [
38
+ # Clients
39
+ "HeleketPayment",
40
+ "HeleketPayout",
41
+ # Configuration
42
+ "ClientOptions",
43
+ "DEFAULT_BASE_URL",
44
+ "DEFAULT_TIMEOUT_SECONDS",
45
+ # Webhook
46
+ "WebhookVerifier",
47
+ "WebhookPayload",
48
+ # Enums
49
+ "PaymentStatus",
50
+ "PayoutStatus",
51
+ "AmlLinkStatus",
52
+ "CourseSource",
53
+ # Errors
54
+ "HeleketError",
55
+ "ApiError",
56
+ "ValidationError",
57
+ "HttpError",
58
+ "SignatureError",
59
+ # Transport
60
+ "Transport",
61
+ "TransportResponse",
62
+ "RequestsTransport",
63
+ # Debug
64
+ "DebugEntry",
65
+ "DebugSink",
66
+ "stderr_sink",
67
+ # Low-level signature
68
+ "sign",
69
+ "constant_time_equal",
70
+ # Metadata
71
+ "__version__",
72
+ ]
@@ -0,0 +1,157 @@
1
+ """Shared HTTP plumbing for :class:`HeleketPayment` and :class:`HeleketPayout`.
2
+
3
+ Encodes the request, signs it, dispatches via the configured ``Transport``,
4
+ dumps to the debug sink when enabled, and translates the response into either
5
+ a typed result or an exception.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from typing import Any
12
+
13
+ from pydantic import BaseModel
14
+
15
+ from ._config import ClientOptions
16
+ from ._debug import DebugEntry
17
+ from ._errors import ApiError, HttpError, ValidationError
18
+ from ._signature import sign
19
+ from ._transport import RequestsTransport, Transport
20
+ from ._version import __version__
21
+
22
+ __all__ = ["BaseClient"]
23
+
24
+ _USER_AGENT = f"heleket-python-sdk/{__version__}"
25
+
26
+
27
+ class BaseClient:
28
+ """Common base for the payment and payout clients.
29
+
30
+ Carries the merchant UUID, API key, and the resolved :class:`ClientOptions`.
31
+ Subclasses call :meth:`_post` with the path and request body.
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ *,
37
+ merchant_id: str,
38
+ api_key: str,
39
+ options: ClientOptions | None = None,
40
+ ) -> None:
41
+ if not merchant_id or not merchant_id.strip():
42
+ raise ValueError("merchant_id must not be empty")
43
+ if not api_key or not api_key.strip():
44
+ raise ValueError("api_key must not be empty")
45
+
46
+ self._merchant_id = merchant_id.strip()
47
+ self._api_key = api_key.strip()
48
+ self._options = options if options is not None else ClientOptions()
49
+ self._transport: Transport = self._options.transport or RequestsTransport()
50
+
51
+ # ------------------------------------------------------------------ #
52
+ # internal helpers
53
+ # ------------------------------------------------------------------ #
54
+
55
+ def _post(self, path: str, params: Any = None) -> Any:
56
+ """Send a signed POST request and return the decoded ``result`` field."""
57
+ return self._send("POST", path, self._encode_body(params))
58
+
59
+ def _get(self, path: str) -> Any:
60
+ """Send a signed GET request and return the decoded ``result`` field.
61
+
62
+ For read-only endpoints that carry all their input in the path (e.g.
63
+ exchange rates). There is no request body, so the signed bytes are the
64
+ empty string — exactly like a POST with no params: ``sign = md5(api_key)``.
65
+ """
66
+ return self._send("GET", path, "")
67
+
68
+ def _send(self, method: str, path: str, body: str) -> Any:
69
+ """Sign the exact body bytes, attach the auth headers, dispatch via the
70
+ transport, and translate the response.
71
+
72
+ Shared by :meth:`_post` and :meth:`_get` so the signing and error
73
+ handling live in exactly one place.
74
+ """
75
+ signature = sign(body, self._api_key)
76
+ url = self._options.base_url + path
77
+
78
+ headers = {
79
+ "merchant": self._merchant_id,
80
+ "sign": signature,
81
+ "Content-Type": "application/json",
82
+ "User-Agent": _USER_AGENT,
83
+ }
84
+
85
+ if self._options.debug:
86
+ self._options.logger(DebugEntry.request(method, url, body))
87
+
88
+ try:
89
+ response = self._transport.round_trip(method, url, headers, body, self._options.timeout)
90
+ except HttpError:
91
+ raise
92
+ except Exception as exc: # noqa: BLE001 — wrap unexpected failures
93
+ raise HttpError(f"transport failed: {exc}") from exc
94
+
95
+ if self._options.debug:
96
+ self._options.logger(DebugEntry.response(response.status_code, response.body))
97
+
98
+ return self._parse_response(response.status_code, response.body)
99
+
100
+ @staticmethod
101
+ def _encode_body(params: Any) -> str:
102
+ """Encode params to JSON, returning ``""`` for null/empty payloads."""
103
+ if params is None:
104
+ return ""
105
+ if isinstance(params, BaseModel):
106
+ # by_alias=True so snake_case wire names beat the Pydantic field
107
+ # names; exclude_none=True so optional fields don't serialise.
108
+ data = params.model_dump(by_alias=True, exclude_none=True)
109
+ elif isinstance(params, dict):
110
+ data = {k: v for k, v in params.items() if v is not None}
111
+ else:
112
+ # Last-ditch — let json.dumps try.
113
+ data = params
114
+
115
+ if not data and not isinstance(data, int | float | bool):
116
+ return ""
117
+ return json.dumps(data, separators=(",", ":"))
118
+
119
+ @staticmethod
120
+ def _parse_response(status_code: int, body: str) -> Any:
121
+ envelope: dict[str, Any] | None
122
+ if body:
123
+ try:
124
+ envelope = json.loads(body)
125
+ if not isinstance(envelope, dict):
126
+ envelope = None
127
+ except json.JSONDecodeError:
128
+ envelope = None
129
+ else:
130
+ envelope = None
131
+
132
+ if status_code == 422:
133
+ fields: dict[str, list[str]] = {}
134
+ if envelope and isinstance(envelope.get("errors"), dict):
135
+ for key, value in envelope["errors"].items():
136
+ if isinstance(value, list):
137
+ fields[key] = [str(v) for v in value]
138
+ else:
139
+ fields[key] = [str(value)]
140
+ summary = _summarise_fields(fields)
141
+ raise ValidationError(f"validation failed: {summary}", status_code, fields, body)
142
+
143
+ if envelope is None:
144
+ raise ApiError(f"non-JSON response (HTTP {status_code})", status_code, body)
145
+
146
+ state = envelope.get("state")
147
+ if state != 0:
148
+ message = envelope.get("message", f"api error (state={state})")
149
+ raise ApiError(str(message), status_code, body)
150
+
151
+ return envelope.get("result")
152
+
153
+
154
+ def _summarise_fields(fields: dict[str, list[str]]) -> str:
155
+ if not fields:
156
+ return "(no field details)"
157
+ return "; ".join(f"{k} ({', '.join(v)})" for k, v in fields.items())