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 ADDED
@@ -0,0 +1,12 @@
1
+ node_modules/
2
+ .env
3
+ .env.*
4
+ !.env.example
5
+ data/
6
+ .tmp/
7
+ artifacts/
8
+ .vercel/
9
+ dist/
10
+ __pycache__/
11
+ *.pyc
12
+ supabase/.temp/
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