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.
- heleket_sdk-0.3.0/CHANGELOG.md +48 -0
- heleket_sdk-0.3.0/LICENSE +21 -0
- heleket_sdk-0.3.0/PKG-INFO +128 -0
- heleket_sdk-0.3.0/README.md +97 -0
- heleket_sdk-0.3.0/UPGRADING.md +36 -0
- heleket_sdk-0.3.0/pyproject.toml +97 -0
- heleket_sdk-0.3.0/src/heleket_sdk/__init__.py +72 -0
- heleket_sdk-0.3.0/src/heleket_sdk/_base_client.py +157 -0
- heleket_sdk-0.3.0/src/heleket_sdk/_cli.py +137 -0
- heleket_sdk-0.3.0/src/heleket_sdk/_config.py +43 -0
- heleket_sdk-0.3.0/src/heleket_sdk/_debug.py +63 -0
- heleket_sdk-0.3.0/src/heleket_sdk/_errors.py +67 -0
- heleket_sdk-0.3.0/src/heleket_sdk/_signature.py +46 -0
- heleket_sdk-0.3.0/src/heleket_sdk/_status.py +128 -0
- heleket_sdk-0.3.0/src/heleket_sdk/_transport.py +93 -0
- heleket_sdk-0.3.0/src/heleket_sdk/_version.py +11 -0
- heleket_sdk-0.3.0/src/heleket_sdk/payment.py +184 -0
- heleket_sdk-0.3.0/src/heleket_sdk/payout.py +110 -0
- heleket_sdk-0.3.0/src/heleket_sdk/py.typed +0 -0
- heleket_sdk-0.3.0/src/heleket_sdk/types.py +417 -0
- heleket_sdk-0.3.0/src/heleket_sdk/webhook.py +234 -0
|
@@ -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())
|