lunipay 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.
- lunipay-0.1.0/.gitignore +33 -0
- lunipay-0.1.0/PKG-INFO +129 -0
- lunipay-0.1.0/README.md +101 -0
- lunipay-0.1.0/lunipay/__init__.py +46 -0
- lunipay-0.1.0/lunipay/_api_requestor.py +131 -0
- lunipay-0.1.0/lunipay/api_resources/__init__.py +1 -0
- lunipay-0.1.0/lunipay/api_resources/_abstract.py +161 -0
- lunipay-0.1.0/lunipay/api_resources/checkout_session.py +22 -0
- lunipay-0.1.0/lunipay/api_resources/customer.py +18 -0
- lunipay-0.1.0/lunipay/api_resources/event.py +6 -0
- lunipay-0.1.0/lunipay/api_resources/invoice.py +48 -0
- lunipay-0.1.0/lunipay/api_resources/payment.py +27 -0
- lunipay-0.1.0/lunipay/api_resources/payment_link.py +18 -0
- lunipay-0.1.0/lunipay/api_resources/webhook_endpoint.py +18 -0
- lunipay-0.1.0/lunipay/error.py +75 -0
- lunipay-0.1.0/lunipay/py.typed +0 -0
- lunipay-0.1.0/lunipay/webhook.py +120 -0
- lunipay-0.1.0/pyproject.toml +48 -0
lunipay-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Node
|
|
2
|
+
node_modules/
|
|
3
|
+
dist/
|
|
4
|
+
*.tgz
|
|
5
|
+
.npm/
|
|
6
|
+
.pnpm-store/
|
|
7
|
+
|
|
8
|
+
# Python
|
|
9
|
+
__pycache__/
|
|
10
|
+
*.py[cod]
|
|
11
|
+
*$py.class
|
|
12
|
+
.venv/
|
|
13
|
+
.pytest_cache/
|
|
14
|
+
*.egg-info/
|
|
15
|
+
build/
|
|
16
|
+
sdist/
|
|
17
|
+
wheelhouse/
|
|
18
|
+
|
|
19
|
+
# Editors & OS
|
|
20
|
+
.DS_Store
|
|
21
|
+
.vscode/
|
|
22
|
+
.idea/
|
|
23
|
+
*.swp
|
|
24
|
+
*.swo
|
|
25
|
+
|
|
26
|
+
# Logs
|
|
27
|
+
*.log
|
|
28
|
+
npm-debug.log*
|
|
29
|
+
yarn-debug.log*
|
|
30
|
+
|
|
31
|
+
# Env
|
|
32
|
+
.env
|
|
33
|
+
.env.local
|
lunipay-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lunipay
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The official LuniPay Python SDK.
|
|
5
|
+
Project-URL: Homepage, https://lunipay.io/docs/sdks/python
|
|
6
|
+
Project-URL: Documentation, https://lunipay.io/docs/sdks/python
|
|
7
|
+
Project-URL: Repository, https://github.com/SammarieoBrown/lunipay-sdk
|
|
8
|
+
Project-URL: Issues, https://github.com/SammarieoBrown/lunipay-sdk/issues
|
|
9
|
+
Author: LuniPay
|
|
10
|
+
License: MIT
|
|
11
|
+
Keywords: api,caribbean,checkout,lunipay,payments,sdk,stripe-alternative
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.8
|
|
25
|
+
Requires-Dist: requests>=2.20
|
|
26
|
+
Requires-Dist: typing-extensions>=4.0; python_version < '3.11'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# lunipay
|
|
30
|
+
|
|
31
|
+
The official LuniPay Python SDK.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install lunipay
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quick start
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
import lunipay
|
|
41
|
+
|
|
42
|
+
lunipay.api_key = "sk_test_YOUR_KEY"
|
|
43
|
+
|
|
44
|
+
session = lunipay.CheckoutSession.create(
|
|
45
|
+
amount=5000,
|
|
46
|
+
currency="usd",
|
|
47
|
+
success_url="https://example.com/thanks?session_id={CHECKOUT_SESSION_ID}",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
print(session["url"])
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Resources
|
|
54
|
+
|
|
55
|
+
Every LuniPay resource is a top-level class on the module:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
lunipay.Customer.create(email="ada@example.com", first_name="Ada", last_name="Lovelace")
|
|
59
|
+
lunipay.Customer.retrieve("cus_01JRZK...")
|
|
60
|
+
lunipay.Customer.modify("cus_01JRZK...", phone="+18765559999")
|
|
61
|
+
lunipay.Customer.delete("cus_01JRZK...")
|
|
62
|
+
|
|
63
|
+
for customer in lunipay.Customer.list().auto_paging_iter():
|
|
64
|
+
print(customer["email"])
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Supported resources: `CheckoutSession`, `Customer`, `Invoice`, `Payment`, `PaymentLink`, `WebhookEndpoint`, `Event`.
|
|
68
|
+
|
|
69
|
+
## Webhooks
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
import lunipay
|
|
73
|
+
|
|
74
|
+
@app.post("/webhook")
|
|
75
|
+
def webhook():
|
|
76
|
+
payload = request.get_data()
|
|
77
|
+
sig = request.headers.get("LuniPay-Signature")
|
|
78
|
+
try:
|
|
79
|
+
event = lunipay.Webhook.construct_event(
|
|
80
|
+
payload=payload,
|
|
81
|
+
sig_header=sig,
|
|
82
|
+
secret="whsec_your_secret",
|
|
83
|
+
)
|
|
84
|
+
except lunipay.error.SignatureVerificationError:
|
|
85
|
+
return "bad signature", 400
|
|
86
|
+
|
|
87
|
+
if event["type"] == "checkout.session.completed":
|
|
88
|
+
# fulfill the order
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
return "", 200
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Errors
|
|
95
|
+
|
|
96
|
+
All LuniPay API errors raise subclasses of `lunipay.error.LuniPayError`:
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
try:
|
|
100
|
+
lunipay.Customer.create(email="bad", first_name="x", last_name="y")
|
|
101
|
+
except lunipay.error.InvalidRequestError as e:
|
|
102
|
+
print(e.code, e.param, str(e))
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Subclasses: `AuthenticationError`, `PermissionError`, `InvalidRequestError`, `IdempotencyError`, `RateLimitError`, `APIError`, `APIConnectionError`.
|
|
106
|
+
|
|
107
|
+
## Idempotency
|
|
108
|
+
|
|
109
|
+
Pass `idempotency_key` to any mutating request:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
import uuid
|
|
113
|
+
|
|
114
|
+
key = str(uuid.uuid4())
|
|
115
|
+
session = lunipay.CheckoutSession.create(
|
|
116
|
+
amount=5000,
|
|
117
|
+
currency="usd",
|
|
118
|
+
success_url="https://example.com/thanks",
|
|
119
|
+
idempotency_key=key,
|
|
120
|
+
)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Full documentation
|
|
124
|
+
|
|
125
|
+
See [lunipay.io/docs/sdks/python](https://lunipay.io/docs/sdks/python).
|
|
126
|
+
|
|
127
|
+
## License
|
|
128
|
+
|
|
129
|
+
MIT
|
lunipay-0.1.0/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# lunipay
|
|
2
|
+
|
|
3
|
+
The official LuniPay Python SDK.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install lunipay
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Quick start
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
import lunipay
|
|
13
|
+
|
|
14
|
+
lunipay.api_key = "sk_test_YOUR_KEY"
|
|
15
|
+
|
|
16
|
+
session = lunipay.CheckoutSession.create(
|
|
17
|
+
amount=5000,
|
|
18
|
+
currency="usd",
|
|
19
|
+
success_url="https://example.com/thanks?session_id={CHECKOUT_SESSION_ID}",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
print(session["url"])
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Resources
|
|
26
|
+
|
|
27
|
+
Every LuniPay resource is a top-level class on the module:
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
lunipay.Customer.create(email="ada@example.com", first_name="Ada", last_name="Lovelace")
|
|
31
|
+
lunipay.Customer.retrieve("cus_01JRZK...")
|
|
32
|
+
lunipay.Customer.modify("cus_01JRZK...", phone="+18765559999")
|
|
33
|
+
lunipay.Customer.delete("cus_01JRZK...")
|
|
34
|
+
|
|
35
|
+
for customer in lunipay.Customer.list().auto_paging_iter():
|
|
36
|
+
print(customer["email"])
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Supported resources: `CheckoutSession`, `Customer`, `Invoice`, `Payment`, `PaymentLink`, `WebhookEndpoint`, `Event`.
|
|
40
|
+
|
|
41
|
+
## Webhooks
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
import lunipay
|
|
45
|
+
|
|
46
|
+
@app.post("/webhook")
|
|
47
|
+
def webhook():
|
|
48
|
+
payload = request.get_data()
|
|
49
|
+
sig = request.headers.get("LuniPay-Signature")
|
|
50
|
+
try:
|
|
51
|
+
event = lunipay.Webhook.construct_event(
|
|
52
|
+
payload=payload,
|
|
53
|
+
sig_header=sig,
|
|
54
|
+
secret="whsec_your_secret",
|
|
55
|
+
)
|
|
56
|
+
except lunipay.error.SignatureVerificationError:
|
|
57
|
+
return "bad signature", 400
|
|
58
|
+
|
|
59
|
+
if event["type"] == "checkout.session.completed":
|
|
60
|
+
# fulfill the order
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
return "", 200
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Errors
|
|
67
|
+
|
|
68
|
+
All LuniPay API errors raise subclasses of `lunipay.error.LuniPayError`:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
try:
|
|
72
|
+
lunipay.Customer.create(email="bad", first_name="x", last_name="y")
|
|
73
|
+
except lunipay.error.InvalidRequestError as e:
|
|
74
|
+
print(e.code, e.param, str(e))
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Subclasses: `AuthenticationError`, `PermissionError`, `InvalidRequestError`, `IdempotencyError`, `RateLimitError`, `APIError`, `APIConnectionError`.
|
|
78
|
+
|
|
79
|
+
## Idempotency
|
|
80
|
+
|
|
81
|
+
Pass `idempotency_key` to any mutating request:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
import uuid
|
|
85
|
+
|
|
86
|
+
key = str(uuid.uuid4())
|
|
87
|
+
session = lunipay.CheckoutSession.create(
|
|
88
|
+
amount=5000,
|
|
89
|
+
currency="usd",
|
|
90
|
+
success_url="https://example.com/thanks",
|
|
91
|
+
idempotency_key=key,
|
|
92
|
+
)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Full documentation
|
|
96
|
+
|
|
97
|
+
See [lunipay.io/docs/sdks/python](https://lunipay.io/docs/sdks/python).
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
MIT
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LuniPay Python SDK.
|
|
3
|
+
|
|
4
|
+
import lunipay
|
|
5
|
+
lunipay.api_key = "sk_test_..."
|
|
6
|
+
session = lunipay.CheckoutSession.create(amount=5000, currency="usd", ...)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
api_key: Optional[str] = None
|
|
12
|
+
api_base: str = "https://lunipay.io/api"
|
|
13
|
+
api_version: str = "2026-04-14"
|
|
14
|
+
|
|
15
|
+
__version__ = "0.1.0"
|
|
16
|
+
|
|
17
|
+
# Resource classes
|
|
18
|
+
from .api_resources.checkout_session import CheckoutSession # noqa: E402
|
|
19
|
+
from .api_resources.customer import Customer # noqa: E402
|
|
20
|
+
from .api_resources.invoice import Invoice # noqa: E402
|
|
21
|
+
from .api_resources.payment_link import PaymentLink # noqa: E402
|
|
22
|
+
from .api_resources.payment import Payment # noqa: E402
|
|
23
|
+
from .api_resources.webhook_endpoint import WebhookEndpoint # noqa: E402
|
|
24
|
+
from .api_resources.event import Event # noqa: E402
|
|
25
|
+
|
|
26
|
+
# Webhook helpers
|
|
27
|
+
from .webhook import Webhook # noqa: E402
|
|
28
|
+
|
|
29
|
+
# Error namespace — users catch `lunipay.error.InvalidRequestError`, etc.
|
|
30
|
+
from . import error # noqa: E402
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"api_key",
|
|
34
|
+
"api_base",
|
|
35
|
+
"api_version",
|
|
36
|
+
"__version__",
|
|
37
|
+
"CheckoutSession",
|
|
38
|
+
"Customer",
|
|
39
|
+
"Invoice",
|
|
40
|
+
"PaymentLink",
|
|
41
|
+
"Payment",
|
|
42
|
+
"WebhookEndpoint",
|
|
43
|
+
"Event",
|
|
44
|
+
"Webhook",
|
|
45
|
+
"error",
|
|
46
|
+
]
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Thin HTTP client wrapper for LuniPay API calls.
|
|
3
|
+
|
|
4
|
+
All resource classes route through `APIRequestor.request()`, which
|
|
5
|
+
handles auth headers, body serialization, error envelope parsing, and
|
|
6
|
+
exception mapping.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Any, Dict, Optional
|
|
10
|
+
|
|
11
|
+
import requests
|
|
12
|
+
|
|
13
|
+
from . import error as err
|
|
14
|
+
|
|
15
|
+
_USER_AGENT = "lunipay-python/0.1.0"
|
|
16
|
+
_DEFAULT_TIMEOUT = 30 # seconds
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def request(
|
|
20
|
+
method: str,
|
|
21
|
+
url: str,
|
|
22
|
+
params: Optional[Dict[str, Any]] = None,
|
|
23
|
+
api_key: Optional[str] = None,
|
|
24
|
+
idempotency_key: Optional[str] = None,
|
|
25
|
+
) -> Any:
|
|
26
|
+
"""
|
|
27
|
+
Make an HTTP request to the LuniPay API.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
method: 'get' | 'post' | 'patch' | 'delete'
|
|
31
|
+
url: path only (e.g. '/v1/customers')
|
|
32
|
+
params: request body (POST/PATCH) or query params (GET/DELETE)
|
|
33
|
+
api_key: per-request override. Falls back to lunipay.api_key.
|
|
34
|
+
idempotency_key: optional Idempotency-Key header value.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
The parsed JSON response body.
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
lunipay.error.AuthenticationError
|
|
41
|
+
lunipay.error.PermissionError
|
|
42
|
+
lunipay.error.RateLimitError
|
|
43
|
+
lunipay.error.InvalidRequestError
|
|
44
|
+
lunipay.error.IdempotencyError
|
|
45
|
+
lunipay.error.APIError
|
|
46
|
+
lunipay.error.APIConnectionError
|
|
47
|
+
"""
|
|
48
|
+
# Late import so consumer `import lunipay` doesn't fail if the config
|
|
49
|
+
# is mutated after the module loads.
|
|
50
|
+
import lunipay
|
|
51
|
+
|
|
52
|
+
effective_key = api_key or lunipay.api_key
|
|
53
|
+
if not effective_key:
|
|
54
|
+
raise err.AuthenticationError(
|
|
55
|
+
"No API key provided. Set `lunipay.api_key` or pass `api_key=` "
|
|
56
|
+
"to the request."
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
full_url = lunipay.api_base.rstrip("/") + url
|
|
60
|
+
|
|
61
|
+
headers = {
|
|
62
|
+
"Authorization": f"Bearer {effective_key}",
|
|
63
|
+
"Accept": "application/json",
|
|
64
|
+
"User-Agent": _USER_AGENT,
|
|
65
|
+
"LuniPay-Version": lunipay.api_version,
|
|
66
|
+
}
|
|
67
|
+
if idempotency_key:
|
|
68
|
+
headers["Idempotency-Key"] = idempotency_key
|
|
69
|
+
|
|
70
|
+
method_lower = method.lower()
|
|
71
|
+
json_body: Optional[Dict[str, Any]] = None
|
|
72
|
+
query: Optional[Dict[str, Any]] = None
|
|
73
|
+
|
|
74
|
+
if method_lower in ("get", "delete"):
|
|
75
|
+
query = params or None
|
|
76
|
+
else:
|
|
77
|
+
headers["Content-Type"] = "application/json"
|
|
78
|
+
json_body = params or {}
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
resp = requests.request(
|
|
82
|
+
method_lower.upper(),
|
|
83
|
+
full_url,
|
|
84
|
+
headers=headers,
|
|
85
|
+
params=query,
|
|
86
|
+
json=json_body,
|
|
87
|
+
timeout=_DEFAULT_TIMEOUT,
|
|
88
|
+
)
|
|
89
|
+
except requests.exceptions.Timeout as e:
|
|
90
|
+
raise err.APIConnectionError(
|
|
91
|
+
f"Request timed out after {_DEFAULT_TIMEOUT}s: {e}"
|
|
92
|
+
)
|
|
93
|
+
except requests.exceptions.ConnectionError as e:
|
|
94
|
+
raise err.APIConnectionError(f"Failed to connect to LuniPay API: {e}")
|
|
95
|
+
except requests.exceptions.RequestException as e:
|
|
96
|
+
raise err.APIConnectionError(f"Request failed: {e}")
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
body = resp.json() if resp.content else {}
|
|
100
|
+
except ValueError:
|
|
101
|
+
body = {}
|
|
102
|
+
|
|
103
|
+
if 200 <= resp.status_code < 300:
|
|
104
|
+
return body
|
|
105
|
+
|
|
106
|
+
_raise_for_error(resp.status_code, body)
|
|
107
|
+
return None # unreachable — _raise_for_error always raises
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _raise_for_error(status: int, body: Any) -> None:
|
|
111
|
+
envelope = body.get("error", {}) if isinstance(body, dict) else {}
|
|
112
|
+
message = envelope.get("message") or f"HTTP {status}"
|
|
113
|
+
err_type = envelope.get("type") or ""
|
|
114
|
+
code = envelope.get("code")
|
|
115
|
+
param = envelope.get("param")
|
|
116
|
+
|
|
117
|
+
if status == 401:
|
|
118
|
+
raise err.AuthenticationError(message, http_status=status, json_body=body)
|
|
119
|
+
if status == 403:
|
|
120
|
+
raise err.PermissionError(message, http_status=status, json_body=body)
|
|
121
|
+
if status == 429:
|
|
122
|
+
raise err.RateLimitError(message, http_status=status, json_body=body)
|
|
123
|
+
if status == 409 and err_type == "idempotency_error":
|
|
124
|
+
raise err.IdempotencyError(
|
|
125
|
+
message, param=param, code=code, http_status=status, json_body=body
|
|
126
|
+
)
|
|
127
|
+
if 400 <= status < 500:
|
|
128
|
+
raise err.InvalidRequestError(
|
|
129
|
+
message, param=param, code=code, http_status=status, json_body=body
|
|
130
|
+
)
|
|
131
|
+
raise err.APIError(message, http_status=status, json_body=body)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""LuniPay API resource classes."""
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Resource base classes and mixins. Resource classes compose these to expose
|
|
3
|
+
`create / retrieve / list / modify / delete` + custom actions.
|
|
4
|
+
|
|
5
|
+
Mirrors Stripe's Python SDK pattern so developers migrating from Stripe
|
|
6
|
+
feel immediately at home.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Any, Dict, Iterator, Optional
|
|
10
|
+
|
|
11
|
+
from .._api_requestor import request as _api_request
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ListObject:
|
|
15
|
+
"""
|
|
16
|
+
Cursor-paginated list response. Supports `auto_paging_iter()` for
|
|
17
|
+
seamless iteration across pages.
|
|
18
|
+
|
|
19
|
+
for customer in lunipay.Customer.list().auto_paging_iter():
|
|
20
|
+
print(customer["email"])
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
data: list,
|
|
26
|
+
has_more: bool,
|
|
27
|
+
url: str,
|
|
28
|
+
resource_cls: type,
|
|
29
|
+
api_key: Optional[str],
|
|
30
|
+
) -> None:
|
|
31
|
+
self.data = data
|
|
32
|
+
self.has_more = has_more
|
|
33
|
+
self.url = url
|
|
34
|
+
self._resource_cls = resource_cls
|
|
35
|
+
self._api_key = api_key
|
|
36
|
+
|
|
37
|
+
def __iter__(self) -> Iterator[Any]:
|
|
38
|
+
return iter(self.data)
|
|
39
|
+
|
|
40
|
+
def __len__(self) -> int:
|
|
41
|
+
return len(self.data)
|
|
42
|
+
|
|
43
|
+
def auto_paging_iter(self) -> Iterator[Any]:
|
|
44
|
+
page: "ListObject" = self
|
|
45
|
+
while True:
|
|
46
|
+
for item in page.data:
|
|
47
|
+
yield item
|
|
48
|
+
if not page.has_more or not page.data:
|
|
49
|
+
return
|
|
50
|
+
last = page.data[-1]
|
|
51
|
+
last_id = last.get("id") if isinstance(last, dict) else None
|
|
52
|
+
if not last_id:
|
|
53
|
+
return
|
|
54
|
+
page = self._resource_cls.list(
|
|
55
|
+
api_key=self._api_key, starting_after=last_id
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class APIResource:
|
|
60
|
+
"""Base class for all API resources."""
|
|
61
|
+
|
|
62
|
+
OBJECT_NAME: str = ""
|
|
63
|
+
RESOURCE_URL: str = ""
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def _static_request(
|
|
67
|
+
cls,
|
|
68
|
+
method: str,
|
|
69
|
+
url: str,
|
|
70
|
+
params: Optional[Dict[str, Any]] = None,
|
|
71
|
+
api_key: Optional[str] = None,
|
|
72
|
+
idempotency_key: Optional[str] = None,
|
|
73
|
+
) -> Any:
|
|
74
|
+
return _api_request(
|
|
75
|
+
method=method,
|
|
76
|
+
url=url,
|
|
77
|
+
params=params,
|
|
78
|
+
api_key=api_key,
|
|
79
|
+
idempotency_key=idempotency_key,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def retrieve(cls, id: str, api_key: Optional[str] = None) -> Any:
|
|
84
|
+
return cls._static_request(
|
|
85
|
+
"get", f"{cls.RESOURCE_URL}/{id}", api_key=api_key
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class CreateableResource:
|
|
90
|
+
"""Mixin: POST {RESOURCE_URL} → create."""
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def create(
|
|
94
|
+
cls,
|
|
95
|
+
api_key: Optional[str] = None,
|
|
96
|
+
idempotency_key: Optional[str] = None,
|
|
97
|
+
**params: Any,
|
|
98
|
+
) -> Any:
|
|
99
|
+
return cls._static_request( # type: ignore[attr-defined]
|
|
100
|
+
"post",
|
|
101
|
+
cls.RESOURCE_URL, # type: ignore[attr-defined]
|
|
102
|
+
params=params,
|
|
103
|
+
api_key=api_key,
|
|
104
|
+
idempotency_key=idempotency_key,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class ListableResource:
|
|
109
|
+
"""Mixin: GET {RESOURCE_URL} → list with cursor pagination."""
|
|
110
|
+
|
|
111
|
+
@classmethod
|
|
112
|
+
def list(
|
|
113
|
+
cls,
|
|
114
|
+
api_key: Optional[str] = None,
|
|
115
|
+
**params: Any,
|
|
116
|
+
) -> ListObject:
|
|
117
|
+
body = cls._static_request( # type: ignore[attr-defined]
|
|
118
|
+
"get",
|
|
119
|
+
cls.RESOURCE_URL, # type: ignore[attr-defined]
|
|
120
|
+
params=params,
|
|
121
|
+
api_key=api_key,
|
|
122
|
+
)
|
|
123
|
+
return ListObject(
|
|
124
|
+
data=body.get("data", []),
|
|
125
|
+
has_more=bool(body.get("has_more", False)),
|
|
126
|
+
url=body.get("url", cls.RESOURCE_URL), # type: ignore[attr-defined]
|
|
127
|
+
resource_cls=cls,
|
|
128
|
+
api_key=api_key,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class UpdateableResource:
|
|
133
|
+
"""Mixin: PATCH {RESOURCE_URL}/{id} → modify."""
|
|
134
|
+
|
|
135
|
+
@classmethod
|
|
136
|
+
def modify(
|
|
137
|
+
cls,
|
|
138
|
+
id: str,
|
|
139
|
+
api_key: Optional[str] = None,
|
|
140
|
+
idempotency_key: Optional[str] = None,
|
|
141
|
+
**params: Any,
|
|
142
|
+
) -> Any:
|
|
143
|
+
return cls._static_request( # type: ignore[attr-defined]
|
|
144
|
+
"patch",
|
|
145
|
+
f"{cls.RESOURCE_URL}/{id}", # type: ignore[attr-defined]
|
|
146
|
+
params=params,
|
|
147
|
+
api_key=api_key,
|
|
148
|
+
idempotency_key=idempotency_key,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class DeleteableResource:
|
|
153
|
+
"""Mixin: DELETE {RESOURCE_URL}/{id}."""
|
|
154
|
+
|
|
155
|
+
@classmethod
|
|
156
|
+
def delete(cls, id: str, api_key: Optional[str] = None) -> Any:
|
|
157
|
+
return cls._static_request( # type: ignore[attr-defined]
|
|
158
|
+
"delete",
|
|
159
|
+
f"{cls.RESOURCE_URL}/{id}", # type: ignore[attr-defined]
|
|
160
|
+
api_key=api_key,
|
|
161
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
|
|
3
|
+
from ._abstract import APIResource, CreateableResource, ListableResource
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CheckoutSession(CreateableResource, ListableResource, APIResource):
|
|
7
|
+
OBJECT_NAME = "checkout_session"
|
|
8
|
+
RESOURCE_URL = "/v1/checkout/sessions"
|
|
9
|
+
|
|
10
|
+
@classmethod
|
|
11
|
+
def expire(
|
|
12
|
+
cls,
|
|
13
|
+
id: str,
|
|
14
|
+
api_key: Optional[str] = None,
|
|
15
|
+
idempotency_key: Optional[str] = None,
|
|
16
|
+
) -> Any:
|
|
17
|
+
return cls._static_request(
|
|
18
|
+
"post",
|
|
19
|
+
f"{cls.RESOURCE_URL}/{id}/expire",
|
|
20
|
+
api_key=api_key,
|
|
21
|
+
idempotency_key=idempotency_key,
|
|
22
|
+
)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from ._abstract import (
|
|
2
|
+
APIResource,
|
|
3
|
+
CreateableResource,
|
|
4
|
+
DeleteableResource,
|
|
5
|
+
ListableResource,
|
|
6
|
+
UpdateableResource,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Customer(
|
|
11
|
+
CreateableResource,
|
|
12
|
+
ListableResource,
|
|
13
|
+
UpdateableResource,
|
|
14
|
+
DeleteableResource,
|
|
15
|
+
APIResource,
|
|
16
|
+
):
|
|
17
|
+
OBJECT_NAME = "customer"
|
|
18
|
+
RESOURCE_URL = "/v1/customers"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
|
|
3
|
+
from ._abstract import (
|
|
4
|
+
APIResource,
|
|
5
|
+
CreateableResource,
|
|
6
|
+
DeleteableResource,
|
|
7
|
+
ListableResource,
|
|
8
|
+
UpdateableResource,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Invoice(
|
|
13
|
+
CreateableResource,
|
|
14
|
+
ListableResource,
|
|
15
|
+
UpdateableResource,
|
|
16
|
+
DeleteableResource,
|
|
17
|
+
APIResource,
|
|
18
|
+
):
|
|
19
|
+
OBJECT_NAME = "invoice"
|
|
20
|
+
RESOURCE_URL = "/v1/invoices"
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def send(
|
|
24
|
+
cls,
|
|
25
|
+
id: str,
|
|
26
|
+
api_key: Optional[str] = None,
|
|
27
|
+
idempotency_key: Optional[str] = None,
|
|
28
|
+
) -> Any:
|
|
29
|
+
return cls._static_request(
|
|
30
|
+
"post",
|
|
31
|
+
f"{cls.RESOURCE_URL}/{id}/send",
|
|
32
|
+
api_key=api_key,
|
|
33
|
+
idempotency_key=idempotency_key,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def void(
|
|
38
|
+
cls,
|
|
39
|
+
id: str,
|
|
40
|
+
api_key: Optional[str] = None,
|
|
41
|
+
idempotency_key: Optional[str] = None,
|
|
42
|
+
) -> Any:
|
|
43
|
+
return cls._static_request(
|
|
44
|
+
"post",
|
|
45
|
+
f"{cls.RESOURCE_URL}/{id}/void",
|
|
46
|
+
api_key=api_key,
|
|
47
|
+
idempotency_key=idempotency_key,
|
|
48
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
|
|
3
|
+
from ._abstract import APIResource, ListableResource
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Payment(ListableResource, APIResource):
|
|
7
|
+
OBJECT_NAME = "payment"
|
|
8
|
+
RESOURCE_URL = "/v1/payments"
|
|
9
|
+
|
|
10
|
+
@classmethod
|
|
11
|
+
def refund(
|
|
12
|
+
cls,
|
|
13
|
+
id: str,
|
|
14
|
+
amount: Optional[int] = None,
|
|
15
|
+
api_key: Optional[str] = None,
|
|
16
|
+
idempotency_key: Optional[str] = None,
|
|
17
|
+
) -> Any:
|
|
18
|
+
params = {}
|
|
19
|
+
if amount is not None:
|
|
20
|
+
params["amount"] = amount
|
|
21
|
+
return cls._static_request(
|
|
22
|
+
"post",
|
|
23
|
+
f"{cls.RESOURCE_URL}/{id}/refund",
|
|
24
|
+
params=params,
|
|
25
|
+
api_key=api_key,
|
|
26
|
+
idempotency_key=idempotency_key,
|
|
27
|
+
)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from ._abstract import (
|
|
2
|
+
APIResource,
|
|
3
|
+
CreateableResource,
|
|
4
|
+
DeleteableResource,
|
|
5
|
+
ListableResource,
|
|
6
|
+
UpdateableResource,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PaymentLink(
|
|
11
|
+
CreateableResource,
|
|
12
|
+
ListableResource,
|
|
13
|
+
UpdateableResource,
|
|
14
|
+
DeleteableResource,
|
|
15
|
+
APIResource,
|
|
16
|
+
):
|
|
17
|
+
OBJECT_NAME = "payment_link"
|
|
18
|
+
RESOURCE_URL = "/v1/payment-links"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from ._abstract import (
|
|
2
|
+
APIResource,
|
|
3
|
+
CreateableResource,
|
|
4
|
+
DeleteableResource,
|
|
5
|
+
ListableResource,
|
|
6
|
+
UpdateableResource,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class WebhookEndpoint(
|
|
11
|
+
CreateableResource,
|
|
12
|
+
ListableResource,
|
|
13
|
+
UpdateableResource,
|
|
14
|
+
DeleteableResource,
|
|
15
|
+
APIResource,
|
|
16
|
+
):
|
|
17
|
+
OBJECT_NAME = "webhook_endpoint"
|
|
18
|
+
RESOURCE_URL = "/v1/webhook-endpoints"
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LuniPay SDK exception hierarchy.
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
lunipay.CheckoutSession.create(amount=-1, currency="usd", ...)
|
|
6
|
+
except lunipay.error.InvalidRequestError as e:
|
|
7
|
+
print(e.param, e.code, e.message)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Any, Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LuniPayError(Exception):
|
|
14
|
+
"""Base class for all SDK errors."""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
message: Optional[str] = None,
|
|
19
|
+
http_status: Optional[int] = None,
|
|
20
|
+
json_body: Optional[Any] = None,
|
|
21
|
+
) -> None:
|
|
22
|
+
self.message = message or "An unknown error occurred"
|
|
23
|
+
self.http_status = http_status
|
|
24
|
+
self.json_body = json_body
|
|
25
|
+
super().__init__(self.message)
|
|
26
|
+
|
|
27
|
+
def __str__(self) -> str:
|
|
28
|
+
return self.message
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class APIError(LuniPayError):
|
|
32
|
+
"""Server-side (5xx) error from the LuniPay API."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class APIConnectionError(LuniPayError):
|
|
36
|
+
"""Network-level error — timeout, DNS failure, TLS error, etc."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class AuthenticationError(LuniPayError):
|
|
40
|
+
"""Invalid or missing API key."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class PermissionError(LuniPayError): # noqa: A001 — intentionally shadowing built-in
|
|
44
|
+
"""API key lacks permission (e.g., publishable key on server endpoint)."""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class RateLimitError(LuniPayError):
|
|
48
|
+
"""Rate limit exceeded (HTTP 429)."""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class InvalidRequestError(LuniPayError):
|
|
52
|
+
"""
|
|
53
|
+
Bad request — invalid params, resource not found, or invalid state
|
|
54
|
+
transition. Carries `param` and `code` from the API error envelope.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
message: Optional[str] = None,
|
|
60
|
+
param: Optional[str] = None,
|
|
61
|
+
code: Optional[str] = None,
|
|
62
|
+
http_status: Optional[int] = None,
|
|
63
|
+
json_body: Optional[Any] = None,
|
|
64
|
+
) -> None:
|
|
65
|
+
self.param = param
|
|
66
|
+
self.code = code
|
|
67
|
+
super().__init__(message, http_status=http_status, json_body=json_body)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class IdempotencyError(InvalidRequestError):
|
|
71
|
+
"""Idempotency key reused with different request parameters (HTTP 409)."""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class SignatureVerificationError(LuniPayError):
|
|
75
|
+
"""Webhook signature verification failed."""
|
|
File without changes
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Webhook signature verification.
|
|
3
|
+
|
|
4
|
+
Mirrors the signing scheme in `src/lib/utils/crypto.ts`:
|
|
5
|
+
|
|
6
|
+
header = f"t={timestamp},v1={hex_hmac_sha256}"
|
|
7
|
+
signed = f"{timestamp}.{payload}"
|
|
8
|
+
hmac = HMAC-SHA256(signed_with=secret).hex()
|
|
9
|
+
|
|
10
|
+
Developers verify inbound webhook requests like so:
|
|
11
|
+
|
|
12
|
+
event = lunipay.Webhook.construct_event(
|
|
13
|
+
payload=request.get_data(),
|
|
14
|
+
sig_header=request.headers["LuniPay-Signature"],
|
|
15
|
+
secret="whsec_your_secret",
|
|
16
|
+
)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import hashlib
|
|
20
|
+
import hmac
|
|
21
|
+
import json
|
|
22
|
+
import time
|
|
23
|
+
from typing import Any, Mapping, Union
|
|
24
|
+
|
|
25
|
+
from .error import SignatureVerificationError
|
|
26
|
+
|
|
27
|
+
DEFAULT_TOLERANCE_SECONDS = 300 # 5 minutes — matches the server default.
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Webhook:
|
|
31
|
+
@staticmethod
|
|
32
|
+
def construct_event(
|
|
33
|
+
payload: Union[bytes, str],
|
|
34
|
+
sig_header: str,
|
|
35
|
+
secret: str,
|
|
36
|
+
tolerance: int = DEFAULT_TOLERANCE_SECONDS,
|
|
37
|
+
) -> Any:
|
|
38
|
+
"""
|
|
39
|
+
Verify a webhook signature and return the parsed event payload.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
payload: Raw request body, bytes or str — MUST be the exact
|
|
43
|
+
bytes LuniPay sent. Do not re-serialize.
|
|
44
|
+
sig_header: Value of the `LuniPay-Signature` header.
|
|
45
|
+
secret: The endpoint's signing secret (whsec_...).
|
|
46
|
+
tolerance: Maximum age of the timestamp in seconds.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
The parsed JSON event dict (with `id`, `type`, `data`, etc.).
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
lunipay.error.SignatureVerificationError on any failure.
|
|
53
|
+
"""
|
|
54
|
+
if isinstance(payload, bytes):
|
|
55
|
+
payload_str = payload.decode("utf-8", errors="strict")
|
|
56
|
+
else:
|
|
57
|
+
payload_str = payload
|
|
58
|
+
|
|
59
|
+
if not sig_header:
|
|
60
|
+
raise SignatureVerificationError("Missing LuniPay-Signature header")
|
|
61
|
+
|
|
62
|
+
timestamp, signatures = _parse_header(sig_header)
|
|
63
|
+
if timestamp is None:
|
|
64
|
+
raise SignatureVerificationError(
|
|
65
|
+
"Unable to extract timestamp from LuniPay-Signature header"
|
|
66
|
+
)
|
|
67
|
+
if not signatures:
|
|
68
|
+
raise SignatureVerificationError(
|
|
69
|
+
"Unable to extract v1 signatures from LuniPay-Signature header"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
signed = f"{timestamp}.{payload_str}"
|
|
73
|
+
expected = hmac.new(
|
|
74
|
+
secret.encode("utf-8"), signed.encode("utf-8"), hashlib.sha256
|
|
75
|
+
).hexdigest()
|
|
76
|
+
|
|
77
|
+
if not any(hmac.compare_digest(expected, sig) for sig in signatures):
|
|
78
|
+
raise SignatureVerificationError(
|
|
79
|
+
"No valid signature found for the payload"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
now = int(time.time())
|
|
83
|
+
if abs(now - timestamp) > tolerance:
|
|
84
|
+
raise SignatureVerificationError(
|
|
85
|
+
f"Timestamp outside tolerance window "
|
|
86
|
+
f"(tolerance={tolerance}s, drift={abs(now - timestamp)}s)"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
return json.loads(payload_str)
|
|
91
|
+
except json.JSONDecodeError as e:
|
|
92
|
+
raise SignatureVerificationError(f"Invalid JSON payload: {e}")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _parse_header(
|
|
96
|
+
header: str,
|
|
97
|
+
) -> "tuple[int | None, list[str]]":
|
|
98
|
+
"""Parse `t=<ts>,v1=<sig>[,v1=<sig2>...]` into (timestamp, [signatures])."""
|
|
99
|
+
timestamp: "int | None" = None
|
|
100
|
+
signatures: list[str] = []
|
|
101
|
+
for part in header.split(","):
|
|
102
|
+
part = part.strip()
|
|
103
|
+
if "=" not in part:
|
|
104
|
+
continue
|
|
105
|
+
k, _, v = part.partition("=")
|
|
106
|
+
k = k.strip()
|
|
107
|
+
v = v.strip()
|
|
108
|
+
if k == "t":
|
|
109
|
+
try:
|
|
110
|
+
timestamp = int(v)
|
|
111
|
+
except ValueError:
|
|
112
|
+
return None, []
|
|
113
|
+
elif k == "v1":
|
|
114
|
+
signatures.append(v)
|
|
115
|
+
return timestamp, signatures
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# Silence an unused-import warning under strict type checkers; Mapping is
|
|
119
|
+
# imported for potential future typed-event support.
|
|
120
|
+
_ = Mapping
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.18"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "lunipay"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "The official LuniPay Python SDK."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
authors = [{ name = "LuniPay" }]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"requests>=2.20",
|
|
15
|
+
"typing_extensions>=4.0; python_version<'3.11'",
|
|
16
|
+
]
|
|
17
|
+
keywords = ["lunipay", "payments", "checkout", "stripe-alternative", "caribbean", "api", "sdk"]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 4 - Beta",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.8",
|
|
23
|
+
"Programming Language :: Python :: 3.9",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Programming Language :: Python :: 3.13",
|
|
28
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
29
|
+
"Topic :: Office/Business :: Financial",
|
|
30
|
+
"Typing :: Typed",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://lunipay.io/docs/sdks/python"
|
|
35
|
+
Documentation = "https://lunipay.io/docs/sdks/python"
|
|
36
|
+
Repository = "https://github.com/SammarieoBrown/lunipay-sdk"
|
|
37
|
+
Issues = "https://github.com/SammarieoBrown/lunipay-sdk/issues"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
packages = ["lunipay"]
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.sdist]
|
|
43
|
+
include = [
|
|
44
|
+
"lunipay",
|
|
45
|
+
"README.md",
|
|
46
|
+
"LICENSE",
|
|
47
|
+
"pyproject.toml",
|
|
48
|
+
]
|