axlet 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.
- axlet-0.1.0/.gitignore +12 -0
- axlet-0.1.0/LICENSE +1 -0
- axlet-0.1.0/PKG-INFO +61 -0
- axlet-0.1.0/README.md +48 -0
- axlet-0.1.0/pyproject.toml +23 -0
- axlet-0.1.0/src/axlet/__init__.py +136 -0
- axlet-0.1.0/src/axlet/py.typed +0 -0
axlet-0.1.0/.gitignore
ADDED
axlet-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
All rights reserved. This release does not grant an open-source license.
|
axlet-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: axlet
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for the Axlet subscription and credits API
|
|
5
|
+
Project-URL: Homepage, https://axlet.app
|
|
6
|
+
License-Expression: LicenseRef-Proprietary
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: axlet,credits,sdk,subscriptions
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# Axlet Python SDK
|
|
15
|
+
|
|
16
|
+
Server-side Python client for the Axlet subscription, entitlement, credits, and purchase APIs. Python 3.10 or later; no runtime dependencies.
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
pip install axlet
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
import os
|
|
24
|
+
from axlet import AxletClient, AxletApiError
|
|
25
|
+
|
|
26
|
+
client = AxletClient(
|
|
27
|
+
api_key=os.environ["AXLET_API_KEY"],
|
|
28
|
+
base_url=os.environ.get("AXLET_API_URL", "http://127.0.0.1:4310"),
|
|
29
|
+
)
|
|
30
|
+
print(client.get_plans())
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Use an Axlet API server you operate or have access to. `axlet.app` is a website, not a hosted public API endpoint. This package does not include the API server. Keep operator API keys on your server.
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
Methods use Python snake_case names. Input and response dictionaries preserve API camelCase fields:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
customer = client.create_customer(
|
|
41
|
+
{"name": "Example", "email": "user@example.com", "externalId": "user-123"},
|
|
42
|
+
idempotency_key="create-user-123",
|
|
43
|
+
)
|
|
44
|
+
access = client.get_access(customer["id"], "report")
|
|
45
|
+
# For each paid operation, confirm successful consumption before doing the work.
|
|
46
|
+
usage = client.consume_usage(
|
|
47
|
+
customer["id"],
|
|
48
|
+
{"feature": "report", "operationId": "report-job-123"},
|
|
49
|
+
idempotency_key="consume-job-123",
|
|
50
|
+
)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Every mutation requires an idempotency key. Keys and operation IDs must contain 8–100 ASCII letters, digits, underscores, or hyphens. Reuse them only for retries of the same operation; generate new IDs for new work. The customer must have the required subscription or credits before consuming paid usage.
|
|
54
|
+
|
|
55
|
+
Supported methods: `get_plans`, `set_plan`, `create_customer`, `register_billing`, `subscribe`, `cancel`, `refund`, `retry`, `get_entitlement`, `get_credits`, `get_access`, `consume_usage`, `cancel_usage`, `create_purchase`, `get_purchase`, `confirm_purchase`, `sync_purchase`, `refund_purchase`, `advance_demo`, and `run_jobs`.
|
|
56
|
+
|
|
57
|
+
HTTP failures and invalid successful responses raise `AxletApiError` with `status` and `body`. Invalid keys raise `ValueError`. Network errors use the standard library's exceptions. Socket operations use a 75-second timeout, configurable with `timeout=`. Requests are synchronous, redirects are rejected, and no automatic retries are performed. Payment behavior depends on the API server configuration; installing this client does not enable production payments.
|
|
58
|
+
|
|
59
|
+
## License
|
|
60
|
+
|
|
61
|
+
All rights reserved. This release does not grant an open-source license.
|
axlet-0.1.0/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Axlet Python SDK
|
|
2
|
+
|
|
3
|
+
Server-side Python client for the Axlet subscription, entitlement, credits, and purchase APIs. Python 3.10 or later; no runtime dependencies.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
pip install axlet
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
import os
|
|
11
|
+
from axlet import AxletClient, AxletApiError
|
|
12
|
+
|
|
13
|
+
client = AxletClient(
|
|
14
|
+
api_key=os.environ["AXLET_API_KEY"],
|
|
15
|
+
base_url=os.environ.get("AXLET_API_URL", "http://127.0.0.1:4310"),
|
|
16
|
+
)
|
|
17
|
+
print(client.get_plans())
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Use an Axlet API server you operate or have access to. `axlet.app` is a website, not a hosted public API endpoint. This package does not include the API server. Keep operator API keys on your server.
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
Methods use Python snake_case names. Input and response dictionaries preserve API camelCase fields:
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
customer = client.create_customer(
|
|
28
|
+
{"name": "Example", "email": "user@example.com", "externalId": "user-123"},
|
|
29
|
+
idempotency_key="create-user-123",
|
|
30
|
+
)
|
|
31
|
+
access = client.get_access(customer["id"], "report")
|
|
32
|
+
# For each paid operation, confirm successful consumption before doing the work.
|
|
33
|
+
usage = client.consume_usage(
|
|
34
|
+
customer["id"],
|
|
35
|
+
{"feature": "report", "operationId": "report-job-123"},
|
|
36
|
+
idempotency_key="consume-job-123",
|
|
37
|
+
)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Every mutation requires an idempotency key. Keys and operation IDs must contain 8–100 ASCII letters, digits, underscores, or hyphens. Reuse them only for retries of the same operation; generate new IDs for new work. The customer must have the required subscription or credits before consuming paid usage.
|
|
41
|
+
|
|
42
|
+
Supported methods: `get_plans`, `set_plan`, `create_customer`, `register_billing`, `subscribe`, `cancel`, `refund`, `retry`, `get_entitlement`, `get_credits`, `get_access`, `consume_usage`, `cancel_usage`, `create_purchase`, `get_purchase`, `confirm_purchase`, `sync_purchase`, `refund_purchase`, `advance_demo`, and `run_jobs`.
|
|
43
|
+
|
|
44
|
+
HTTP failures and invalid successful responses raise `AxletApiError` with `status` and `body`. Invalid keys raise `ValueError`. Network errors use the standard library's exceptions. Socket operations use a 75-second timeout, configurable with `timeout=`. Requests are synchronous, redirects are rejected, and no automatic retries are performed. Payment behavior depends on the API server configuration; installing this client does not enable production payments.
|
|
45
|
+
|
|
46
|
+
## License
|
|
47
|
+
|
|
48
|
+
All rights reserved. This release does not grant an open-source license.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.26,<2"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "axlet"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python client for the Axlet subscription and credits API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "LicenseRef-Proprietary"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
keywords = ["axlet", "sdk", "subscriptions", "credits"]
|
|
14
|
+
classifiers = ["Programming Language :: Python :: 3", "Operating System :: OS Independent"]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://axlet.app"
|
|
18
|
+
|
|
19
|
+
[tool.hatch.build.targets.wheel]
|
|
20
|
+
packages = ["src/axlet"]
|
|
21
|
+
|
|
22
|
+
[tool.hatch.build.targets.sdist]
|
|
23
|
+
include = ["src/axlet", "pyproject.toml", "README.md", "LICENSE"]
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Server-side client for the Axlet API."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from urllib.error import HTTPError
|
|
6
|
+
from urllib.parse import quote, urlencode, urlsplit
|
|
7
|
+
from urllib.request import HTTPRedirectHandler, Request, build_opener
|
|
8
|
+
|
|
9
|
+
__all__ = ["AxletClient", "AxletApiError"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AxletApiError(Exception):
|
|
13
|
+
def __init__(self, status: int, message: str, body: object):
|
|
14
|
+
super().__init__(message)
|
|
15
|
+
self.status = status
|
|
16
|
+
self.body = body
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _NoRedirect(HTTPRedirectHandler):
|
|
20
|
+
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
21
|
+
# 운영자 키가 리다이렉트 대상 서버로 전달되지 않게 합니다.
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _key(value: str) -> str:
|
|
26
|
+
if not isinstance(value, str) or not re.fullmatch(r"[a-zA-Z0-9_-]{8,100}", value):
|
|
27
|
+
raise ValueError("Keys must contain 8-100 ASCII letters, digits, underscores or hyphens.")
|
|
28
|
+
return value
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _customer_path(customer_id: str, action: str) -> str:
|
|
32
|
+
return f"/api/customers/{quote(customer_id, safe='')}/{action}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class AxletClient:
|
|
36
|
+
def __init__(self, api_key: str, base_url: str = "http://127.0.0.1:4310", *, timeout: float = 75):
|
|
37
|
+
if not isinstance(api_key, str) or not api_key or "\r" in api_key or "\n" in api_key:
|
|
38
|
+
raise ValueError("A valid api_key is required.")
|
|
39
|
+
parsed = urlsplit(base_url)
|
|
40
|
+
if parsed.scheme not in ("http", "https") or not parsed.hostname or parsed.username or parsed.password or parsed.query or parsed.fragment:
|
|
41
|
+
raise ValueError("base_url must be an HTTP(S) URL without credentials, query or fragment.")
|
|
42
|
+
if timeout <= 0:
|
|
43
|
+
raise ValueError("timeout must be positive.")
|
|
44
|
+
self.base_url = base_url.rstrip("/")
|
|
45
|
+
self._api_key = api_key
|
|
46
|
+
self.timeout = timeout
|
|
47
|
+
self._opener = build_opener(_NoRedirect())
|
|
48
|
+
|
|
49
|
+
def _request(self, path: str, body: dict | None = None, idempotency_key: str | None = None) -> dict:
|
|
50
|
+
headers = {"Authorization": f"Bearer {self._api_key}", "Accept": "application/json"}
|
|
51
|
+
data = None
|
|
52
|
+
if body is not None:
|
|
53
|
+
headers["Idempotency-Key"] = _key(idempotency_key)
|
|
54
|
+
headers["Content-Type"] = "application/json"
|
|
55
|
+
data = json.dumps(body, ensure_ascii=False).encode("utf-8")
|
|
56
|
+
request = Request(self.base_url + path, data=data, headers=headers)
|
|
57
|
+
try:
|
|
58
|
+
response = self._opener.open(request, timeout=self.timeout)
|
|
59
|
+
except HTTPError as error:
|
|
60
|
+
response = error
|
|
61
|
+
with response:
|
|
62
|
+
status = response.code
|
|
63
|
+
text = response.read().decode("utf-8", errors="replace")
|
|
64
|
+
try:
|
|
65
|
+
result = json.loads(text)
|
|
66
|
+
except ValueError:
|
|
67
|
+
result = text
|
|
68
|
+
if not 200 <= status < 300:
|
|
69
|
+
message = result.get("error") if isinstance(result, dict) else result
|
|
70
|
+
if not isinstance(message, str) or not message:
|
|
71
|
+
message = f"Axlet API request failed ({status})."
|
|
72
|
+
raise AxletApiError(status, message, result)
|
|
73
|
+
if not isinstance(result, dict):
|
|
74
|
+
raise AxletApiError(status, "Axlet API did not return a JSON object.", result)
|
|
75
|
+
return result
|
|
76
|
+
|
|
77
|
+
def get_plans(self) -> dict:
|
|
78
|
+
return self._request("/api/plans")
|
|
79
|
+
|
|
80
|
+
def set_plan(self, plan: dict, idempotency_key: str) -> dict:
|
|
81
|
+
return self._request("/api/plans", plan, idempotency_key)
|
|
82
|
+
|
|
83
|
+
def create_customer(self, customer: dict, idempotency_key: str) -> dict:
|
|
84
|
+
return self._request("/api/customers", customer, idempotency_key)
|
|
85
|
+
|
|
86
|
+
def register_billing(self, customer_id: str, idempotency_key: str) -> dict:
|
|
87
|
+
return self._request(_customer_path(customer_id, "billing"), {}, idempotency_key)
|
|
88
|
+
|
|
89
|
+
def subscribe(self, customer_id: str, idempotency_key: str) -> dict:
|
|
90
|
+
return self._request(_customer_path(customer_id, "subscribe"), {}, idempotency_key)
|
|
91
|
+
|
|
92
|
+
def cancel(self, customer_id: str, idempotency_key: str) -> dict:
|
|
93
|
+
return self._request(_customer_path(customer_id, "cancel"), {}, idempotency_key)
|
|
94
|
+
|
|
95
|
+
def refund(self, customer_id: str, idempotency_key: str) -> dict:
|
|
96
|
+
return self._request(_customer_path(customer_id, "refund"), {}, idempotency_key)
|
|
97
|
+
|
|
98
|
+
def retry(self, customer_id: str, idempotency_key: str) -> dict:
|
|
99
|
+
return self._request(_customer_path(customer_id, "retry"), {}, idempotency_key)
|
|
100
|
+
|
|
101
|
+
def get_entitlement(self, customer_id: str) -> dict:
|
|
102
|
+
return self._request(_customer_path(customer_id, "entitlement"))
|
|
103
|
+
|
|
104
|
+
def get_credits(self, customer_id: str) -> dict:
|
|
105
|
+
return self._request(_customer_path(customer_id, "credits"))
|
|
106
|
+
|
|
107
|
+
def get_access(self, customer_id: str, feature: str) -> dict:
|
|
108
|
+
return self._request(_customer_path(customer_id, "access") + "?" + urlencode({"feature": feature}))
|
|
109
|
+
|
|
110
|
+
def consume_usage(self, customer_id: str, usage: dict, idempotency_key: str) -> dict:
|
|
111
|
+
_key(usage.get("operationId"))
|
|
112
|
+
return self._request(_customer_path(customer_id, "usage"), usage, idempotency_key)
|
|
113
|
+
|
|
114
|
+
def cancel_usage(self, customer_id: str, operation_id: str, idempotency_key: str) -> dict:
|
|
115
|
+
return self._request(_customer_path(customer_id, "usage/cancel"), {"operationId": _key(operation_id)}, idempotency_key)
|
|
116
|
+
|
|
117
|
+
def create_purchase(self, purchase: dict, idempotency_key: str) -> dict:
|
|
118
|
+
return self._request("/api/purchases", purchase, idempotency_key)
|
|
119
|
+
|
|
120
|
+
def get_purchase(self, purchase_id: str) -> dict:
|
|
121
|
+
return self._request(f"/api/purchases/{quote(purchase_id, safe='')}")
|
|
122
|
+
|
|
123
|
+
def confirm_purchase(self, purchase_id: str, confirmation: dict, idempotency_key: str) -> dict:
|
|
124
|
+
return self._request(f"/api/purchases/{quote(purchase_id, safe='')}/confirm", confirmation, idempotency_key)
|
|
125
|
+
|
|
126
|
+
def sync_purchase(self, purchase_id: str, idempotency_key: str) -> dict:
|
|
127
|
+
return self._request(f"/api/purchases/{quote(purchase_id, safe='')}/sync", {}, idempotency_key)
|
|
128
|
+
|
|
129
|
+
def refund_purchase(self, purchase_id: str, idempotency_key: str) -> dict:
|
|
130
|
+
return self._request(f"/api/purchases/{quote(purchase_id, safe='')}/refund", {}, idempotency_key)
|
|
131
|
+
|
|
132
|
+
def advance_demo(self, days: int, idempotency_key: str) -> dict:
|
|
133
|
+
return self._request("/api/demo/advance", {"days": days}, idempotency_key)
|
|
134
|
+
|
|
135
|
+
def run_jobs(self, idempotency_key: str) -> dict:
|
|
136
|
+
return self._request("/api/jobs/run", {}, idempotency_key)
|
|
File without changes
|