paiment 0.2.0__py3-none-any.whl
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.
- paiment/__init__.py +78 -0
- paiment/_errors.py +25 -0
- paiment/_http.py +119 -0
- paiment/_models.py +221 -0
- paiment/resources/__init__.py +0 -0
- paiment/resources/plans.py +163 -0
- paiment/resources/subscriptions.py +216 -0
- paiment/resources/usage.py +130 -0
- paiment/resources/users.py +219 -0
- paiment-0.2.0.dist-info/METADATA +166 -0
- paiment-0.2.0.dist-info/RECORD +13 -0
- paiment-0.2.0.dist-info/WHEEL +5 -0
- paiment-0.2.0.dist-info/top_level.txt +1 -0
paiment/__init__.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ._errors import PaimentAuthError, PaimentError, PaimentNotFoundError, PaimentValidationError
|
|
6
|
+
from ._http import AsyncHttpClient, HttpClient
|
|
7
|
+
from ._models import FixedPrice, Page, Plan, Pagination, Subscription, Usage, UsageBasedPrice, User
|
|
8
|
+
from .resources.plans import AsyncPlansResource, PlansResource
|
|
9
|
+
from .resources.subscriptions import AsyncSubscriptionsResource, SubscriptionsResource
|
|
10
|
+
from .resources.usage import AsyncUsageResource, UsageResource
|
|
11
|
+
from .resources.users import AsyncUsersResource, UsersResource
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"PaimentClient",
|
|
15
|
+
"AsyncPaimentClient",
|
|
16
|
+
"PaimentError",
|
|
17
|
+
"PaimentAuthError",
|
|
18
|
+
"PaimentNotFoundError",
|
|
19
|
+
"PaimentValidationError",
|
|
20
|
+
"User",
|
|
21
|
+
"Subscription",
|
|
22
|
+
"Plan",
|
|
23
|
+
"UsageBasedPrice",
|
|
24
|
+
"FixedPrice",
|
|
25
|
+
"Usage",
|
|
26
|
+
"Page",
|
|
27
|
+
"Pagination",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
_DEFAULT_BASE_URL = "https://api.paiment.tech"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class PaimentClient:
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
*,
|
|
37
|
+
api_key: str,
|
|
38
|
+
base_url: str = _DEFAULT_BASE_URL,
|
|
39
|
+
workspace_id: str | None = None,
|
|
40
|
+
) -> None:
|
|
41
|
+
self._http = HttpClient(api_key=api_key, base_url=base_url, workspace_id=workspace_id)
|
|
42
|
+
self.users = UsersResource(self._http)
|
|
43
|
+
self.subscriptions = SubscriptionsResource(self._http)
|
|
44
|
+
self.plans = PlansResource(self._http)
|
|
45
|
+
self.usage = UsageResource(self._http)
|
|
46
|
+
|
|
47
|
+
def close(self) -> None:
|
|
48
|
+
self._http.close()
|
|
49
|
+
|
|
50
|
+
def __enter__(self) -> "PaimentClient":
|
|
51
|
+
return self
|
|
52
|
+
|
|
53
|
+
def __exit__(self, *_: Any) -> None:
|
|
54
|
+
self.close()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class AsyncPaimentClient:
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
*,
|
|
61
|
+
api_key: str,
|
|
62
|
+
base_url: str = _DEFAULT_BASE_URL,
|
|
63
|
+
workspace_id: str | None = None,
|
|
64
|
+
) -> None:
|
|
65
|
+
self._http = AsyncHttpClient(api_key=api_key, base_url=base_url, workspace_id=workspace_id)
|
|
66
|
+
self.users = AsyncUsersResource(self._http)
|
|
67
|
+
self.subscriptions = AsyncSubscriptionsResource(self._http)
|
|
68
|
+
self.plans = AsyncPlansResource(self._http)
|
|
69
|
+
self.usage = AsyncUsageResource(self._http)
|
|
70
|
+
|
|
71
|
+
async def aclose(self) -> None:
|
|
72
|
+
await self._http.aclose()
|
|
73
|
+
|
|
74
|
+
async def __aenter__(self) -> "AsyncPaimentClient":
|
|
75
|
+
return self
|
|
76
|
+
|
|
77
|
+
async def __aexit__(self, *_: Any) -> None:
|
|
78
|
+
await self.aclose()
|
paiment/_errors.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PaimentError(Exception):
|
|
7
|
+
def __init__(self, message: str, status_code: int | None = None, body: Any = None) -> None:
|
|
8
|
+
super().__init__(message)
|
|
9
|
+
self.status_code = status_code
|
|
10
|
+
self.body = body
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PaimentAuthError(PaimentError):
|
|
14
|
+
def __init__(self, body: Any = None) -> None:
|
|
15
|
+
super().__init__("Authentication failed. Check your API key.", 401, body)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PaimentNotFoundError(PaimentError):
|
|
19
|
+
def __init__(self, resource: str = "Resource") -> None:
|
|
20
|
+
super().__init__(f"{resource} not found.", 404)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PaimentValidationError(PaimentError):
|
|
24
|
+
def __init__(self, body: Any = None) -> None:
|
|
25
|
+
super().__init__("Validation failed.", 400, body)
|
paiment/_http.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from ._errors import PaimentAuthError, PaimentError, PaimentNotFoundError, PaimentValidationError
|
|
8
|
+
|
|
9
|
+
_DEFAULT_TIMEOUT = 30.0
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _raise_for_status(response: httpx.Response) -> None:
|
|
13
|
+
if response.is_success:
|
|
14
|
+
return
|
|
15
|
+
try:
|
|
16
|
+
body: Any = response.json()
|
|
17
|
+
except Exception:
|
|
18
|
+
body = response.text
|
|
19
|
+
|
|
20
|
+
status = response.status_code
|
|
21
|
+
if status == 401:
|
|
22
|
+
raise PaimentAuthError(body)
|
|
23
|
+
if status == 404:
|
|
24
|
+
raise PaimentNotFoundError()
|
|
25
|
+
if status == 400:
|
|
26
|
+
raise PaimentValidationError(body)
|
|
27
|
+
raise PaimentError(f"Request failed with status {status}", status, body)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class HttpClient:
|
|
31
|
+
def __init__(self, api_key: str, base_url: str, workspace_id: str | None = None) -> None:
|
|
32
|
+
headers: dict[str, str] = {"X-Api-Key": api_key, "Content-Type": "application/json"}
|
|
33
|
+
if workspace_id:
|
|
34
|
+
headers["X-Workspace-Id"] = workspace_id
|
|
35
|
+
self.workspace_id = workspace_id
|
|
36
|
+
self._client = httpx.Client(
|
|
37
|
+
base_url=base_url.rstrip("/"),
|
|
38
|
+
headers=headers,
|
|
39
|
+
timeout=_DEFAULT_TIMEOUT,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def close(self) -> None:
|
|
43
|
+
self._client.close()
|
|
44
|
+
|
|
45
|
+
def __enter__(self) -> "HttpClient":
|
|
46
|
+
return self
|
|
47
|
+
|
|
48
|
+
def __exit__(self, *_: Any) -> None:
|
|
49
|
+
self.close()
|
|
50
|
+
|
|
51
|
+
def get(self, path: str, params: dict[str, Any] | None = None) -> Any:
|
|
52
|
+
r = self._client.get(path, params={k: v for k, v in (params or {}).items() if v is not None})
|
|
53
|
+
_raise_for_status(r)
|
|
54
|
+
return r.json() if r.status_code != 204 else None
|
|
55
|
+
|
|
56
|
+
def post(self, path: str, body: Any = None) -> Any:
|
|
57
|
+
r = self._client.post(path, json=body)
|
|
58
|
+
_raise_for_status(r)
|
|
59
|
+
return r.json() if r.status_code != 204 else None
|
|
60
|
+
|
|
61
|
+
def patch(self, path: str, body: Any = None) -> Any:
|
|
62
|
+
r = self._client.patch(path, json=body)
|
|
63
|
+
_raise_for_status(r)
|
|
64
|
+
return r.json() if r.status_code != 204 else None
|
|
65
|
+
|
|
66
|
+
def put(self, path: str, body: Any = None) -> Any:
|
|
67
|
+
r = self._client.put(path, json=body)
|
|
68
|
+
_raise_for_status(r)
|
|
69
|
+
return r.json() if r.status_code != 204 else None
|
|
70
|
+
|
|
71
|
+
def delete(self, path: str) -> None:
|
|
72
|
+
r = self._client.delete(path)
|
|
73
|
+
_raise_for_status(r)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class AsyncHttpClient:
|
|
77
|
+
def __init__(self, api_key: str, base_url: str, workspace_id: str | None = None) -> None:
|
|
78
|
+
headers: dict[str, str] = {"X-Api-Key": api_key, "Content-Type": "application/json"}
|
|
79
|
+
if workspace_id:
|
|
80
|
+
headers["X-Workspace-Id"] = workspace_id
|
|
81
|
+
self.workspace_id = workspace_id
|
|
82
|
+
self._client = httpx.AsyncClient(
|
|
83
|
+
base_url=base_url.rstrip("/"),
|
|
84
|
+
headers=headers,
|
|
85
|
+
timeout=_DEFAULT_TIMEOUT,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
async def aclose(self) -> None:
|
|
89
|
+
await self._client.aclose()
|
|
90
|
+
|
|
91
|
+
async def __aenter__(self) -> "AsyncHttpClient":
|
|
92
|
+
return self
|
|
93
|
+
|
|
94
|
+
async def __aexit__(self, *_: Any) -> None:
|
|
95
|
+
await self.aclose()
|
|
96
|
+
|
|
97
|
+
async def get(self, path: str, params: dict[str, Any] | None = None) -> Any:
|
|
98
|
+
r = await self._client.get(path, params={k: v for k, v in (params or {}).items() if v is not None})
|
|
99
|
+
_raise_for_status(r)
|
|
100
|
+
return r.json() if r.status_code != 204 else None
|
|
101
|
+
|
|
102
|
+
async def post(self, path: str, body: Any = None) -> Any:
|
|
103
|
+
r = await self._client.post(path, json=body)
|
|
104
|
+
_raise_for_status(r)
|
|
105
|
+
return r.json() if r.status_code != 204 else None
|
|
106
|
+
|
|
107
|
+
async def patch(self, path: str, body: Any = None) -> Any:
|
|
108
|
+
r = await self._client.patch(path, json=body)
|
|
109
|
+
_raise_for_status(r)
|
|
110
|
+
return r.json() if r.status_code != 204 else None
|
|
111
|
+
|
|
112
|
+
async def put(self, path: str, body: Any = None) -> Any:
|
|
113
|
+
r = await self._client.put(path, json=body)
|
|
114
|
+
_raise_for_status(r)
|
|
115
|
+
return r.json() if r.status_code != 204 else None
|
|
116
|
+
|
|
117
|
+
async def delete(self, path: str) -> None:
|
|
118
|
+
r = await self._client.delete(path)
|
|
119
|
+
_raise_for_status(r)
|
paiment/_models.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Generic, TypeVar
|
|
5
|
+
|
|
6
|
+
T = TypeVar("T")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _get(d: dict[str, Any], key: str, default: Any = None) -> Any:
|
|
10
|
+
return d.get(key, default)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class Subscription:
|
|
15
|
+
id: int
|
|
16
|
+
tenant_id: str
|
|
17
|
+
workspace_id: str
|
|
18
|
+
user_id: str
|
|
19
|
+
plan_id: int | None
|
|
20
|
+
plan_name: str
|
|
21
|
+
status: str
|
|
22
|
+
subscription_start: str
|
|
23
|
+
subscription_end: str | None
|
|
24
|
+
billing_period_start: str | None
|
|
25
|
+
billing_period_end: str | None
|
|
26
|
+
trial_start: str | None
|
|
27
|
+
trial_end: str | None
|
|
28
|
+
price: int
|
|
29
|
+
currency: str
|
|
30
|
+
interval: str
|
|
31
|
+
source: str
|
|
32
|
+
created_at: str
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def from_dict(cls, d: dict[str, Any]) -> "Subscription":
|
|
36
|
+
return cls(
|
|
37
|
+
id=d["id"],
|
|
38
|
+
tenant_id=d.get("tenantId", ""),
|
|
39
|
+
workspace_id=str(d.get("workspaceId", "")),
|
|
40
|
+
user_id=d.get("userId", ""),
|
|
41
|
+
plan_id=_get(d, "planId"),
|
|
42
|
+
plan_name=d.get("planName", ""),
|
|
43
|
+
status=d.get("status", "Active"),
|
|
44
|
+
subscription_start=d.get("subscriptionStart", ""),
|
|
45
|
+
subscription_end=_get(d, "subscriptionEnd"),
|
|
46
|
+
billing_period_start=_get(d, "billingPeriodStart"),
|
|
47
|
+
billing_period_end=_get(d, "billingPeriodEnd"),
|
|
48
|
+
trial_start=_get(d, "trialStart"),
|
|
49
|
+
trial_end=_get(d, "trialEnd"),
|
|
50
|
+
price=d.get("price", 0),
|
|
51
|
+
currency=d.get("currency", ""),
|
|
52
|
+
interval=d.get("interval", ""),
|
|
53
|
+
source=d.get("source", ""),
|
|
54
|
+
created_at=d.get("createdAt", ""),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class User:
|
|
60
|
+
id: int
|
|
61
|
+
user_id: str | None
|
|
62
|
+
tenant_id: str | None
|
|
63
|
+
user_name: str | None
|
|
64
|
+
user_email: str | None
|
|
65
|
+
workspace_id: str | None
|
|
66
|
+
created_at: str
|
|
67
|
+
subscriptions: list[Subscription]
|
|
68
|
+
plan_name: str
|
|
69
|
+
price: int
|
|
70
|
+
currency: str
|
|
71
|
+
interval: str
|
|
72
|
+
subscription_date: str
|
|
73
|
+
subscription_end_date: str | None
|
|
74
|
+
source: str
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def from_dict(cls, d: dict[str, Any]) -> "User":
|
|
78
|
+
return cls(
|
|
79
|
+
id=d["id"],
|
|
80
|
+
user_id=_get(d, "userId"),
|
|
81
|
+
tenant_id=_get(d, "tenantId"),
|
|
82
|
+
user_name=_get(d, "userName"),
|
|
83
|
+
user_email=_get(d, "userEmail"),
|
|
84
|
+
workspace_id=str(_get(d, "workspaceId")) if _get(d, "workspaceId") is not None else None,
|
|
85
|
+
created_at=d.get("createdAt", ""),
|
|
86
|
+
subscriptions=[Subscription.from_dict(s) for s in d.get("subscriptions", [])],
|
|
87
|
+
plan_name=d.get("planName", ""),
|
|
88
|
+
price=d.get("price", 0),
|
|
89
|
+
currency=d.get("currency", ""),
|
|
90
|
+
interval=d.get("interval", ""),
|
|
91
|
+
subscription_date=d.get("subscriptionDate", ""),
|
|
92
|
+
subscription_end_date=_get(d, "subscriptionEndDate"),
|
|
93
|
+
source=d.get("source", ""),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass
|
|
98
|
+
class UsageBasedPrice:
|
|
99
|
+
billable_metric: str
|
|
100
|
+
billing_cycle: str
|
|
101
|
+
recurring_period: str | None
|
|
102
|
+
price_model: str
|
|
103
|
+
per_unit: float
|
|
104
|
+
price_adjustments: dict[str, Any] | None = None
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def from_dict(cls, d: dict[str, Any]) -> "UsageBasedPrice":
|
|
108
|
+
return cls(
|
|
109
|
+
billable_metric=d.get("billableMetric", ""),
|
|
110
|
+
billing_cycle=d.get("billingCycle", ""),
|
|
111
|
+
recurring_period=_get(d, "recurringPeriod"),
|
|
112
|
+
price_model=d.get("priceModel", "unit-pricing"),
|
|
113
|
+
per_unit=float(d.get("perUnit", 0)),
|
|
114
|
+
price_adjustments=_get(d, "priceAdjustments"),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass
|
|
119
|
+
class FixedPrice:
|
|
120
|
+
item: str
|
|
121
|
+
price_model: str
|
|
122
|
+
quantity: int
|
|
123
|
+
per_unit: float
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
def from_dict(cls, d: dict[str, Any]) -> "FixedPrice":
|
|
127
|
+
return cls(
|
|
128
|
+
item=d.get("item", ""),
|
|
129
|
+
price_model=d.get("priceModel", "unit-pricing"),
|
|
130
|
+
quantity=d.get("quantity", 1),
|
|
131
|
+
per_unit=float(d.get("perUnit", 0)),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass
|
|
136
|
+
class Plan:
|
|
137
|
+
id: int
|
|
138
|
+
name: str
|
|
139
|
+
tenant_id: str
|
|
140
|
+
external_plan_id: str | None
|
|
141
|
+
description: str | None
|
|
142
|
+
currency: str
|
|
143
|
+
list_price_cents: int
|
|
144
|
+
usage_based_prices: list[UsageBasedPrice] = field(default_factory=list)
|
|
145
|
+
fixed_prices: list[FixedPrice] = field(default_factory=list)
|
|
146
|
+
|
|
147
|
+
@classmethod
|
|
148
|
+
def from_dict(cls, d: dict[str, Any]) -> "Plan":
|
|
149
|
+
return cls(
|
|
150
|
+
id=d["id"],
|
|
151
|
+
name=d.get("name", ""),
|
|
152
|
+
tenant_id=d.get("tenantId", ""),
|
|
153
|
+
external_plan_id=_get(d, "externalPlanId"),
|
|
154
|
+
description=_get(d, "description"),
|
|
155
|
+
currency=d.get("currency", ""),
|
|
156
|
+
list_price_cents=d.get("listPriceCents", 0),
|
|
157
|
+
usage_based_prices=[
|
|
158
|
+
UsageBasedPrice.from_dict(p) for p in d.get("usageBasedPrices", [])
|
|
159
|
+
],
|
|
160
|
+
fixed_prices=[FixedPrice.from_dict(p) for p in d.get("fixedPrices", [])],
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@dataclass
|
|
165
|
+
class Usage:
|
|
166
|
+
id: int
|
|
167
|
+
user_id: str
|
|
168
|
+
tenant_id: str | None
|
|
169
|
+
plan_id: int | None
|
|
170
|
+
workspace_id: str | None
|
|
171
|
+
input_tokens: int
|
|
172
|
+
output_tokens: int
|
|
173
|
+
llm_provider: str
|
|
174
|
+
llm_model: str
|
|
175
|
+
feature: str | None
|
|
176
|
+
feature_session: str | None
|
|
177
|
+
ts: int | None
|
|
178
|
+
total_cost: float | None
|
|
179
|
+
formatted_cost: str | None
|
|
180
|
+
|
|
181
|
+
@classmethod
|
|
182
|
+
def from_dict(cls, d: dict[str, Any]) -> "Usage":
|
|
183
|
+
return cls(
|
|
184
|
+
id=d["id"],
|
|
185
|
+
user_id=d.get("userId", ""),
|
|
186
|
+
tenant_id=_get(d, "tenantId"),
|
|
187
|
+
plan_id=_get(d, "planId"),
|
|
188
|
+
workspace_id=str(_get(d, "workspaceId")) if _get(d, "workspaceId") is not None else None,
|
|
189
|
+
input_tokens=d.get("inputTokens", 0),
|
|
190
|
+
output_tokens=d.get("outputTokens", 0),
|
|
191
|
+
llm_provider=d.get("llmProvider", ""),
|
|
192
|
+
llm_model=d.get("llmModel", ""),
|
|
193
|
+
feature=_get(d, "feature"),
|
|
194
|
+
feature_session=_get(d, "featureSession"),
|
|
195
|
+
ts=_get(d, "ts"),
|
|
196
|
+
total_cost=_get(d, "totalCost"),
|
|
197
|
+
formatted_cost=_get(d, "formattedCost"),
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@dataclass
|
|
202
|
+
class Pagination:
|
|
203
|
+
current_page: int
|
|
204
|
+
items_per_page: int
|
|
205
|
+
total_items: int
|
|
206
|
+
total_pages: int
|
|
207
|
+
|
|
208
|
+
@classmethod
|
|
209
|
+
def from_dict(cls, d: dict[str, Any]) -> "Pagination":
|
|
210
|
+
return cls(
|
|
211
|
+
current_page=d.get("currentPage", 1),
|
|
212
|
+
items_per_page=d.get("itemsPerPage", 20),
|
|
213
|
+
total_items=d.get("totalItems", 0),
|
|
214
|
+
total_pages=d.get("totalPages", 0),
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@dataclass
|
|
219
|
+
class Page(Generic[T]):
|
|
220
|
+
data: list[T]
|
|
221
|
+
pagination: Pagination
|
|
File without changes
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .._http import AsyncHttpClient, HttpClient
|
|
6
|
+
from .._models import Plan
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _build_plan_body(
|
|
10
|
+
*,
|
|
11
|
+
name: str | None = None,
|
|
12
|
+
currency: str | None = None,
|
|
13
|
+
external_plan_id: str | None = None,
|
|
14
|
+
description: str | None = None,
|
|
15
|
+
list_price_cents: int | None = None,
|
|
16
|
+
tokens: int | None = None,
|
|
17
|
+
usage_based_prices: list[dict[str, Any]] | None = None,
|
|
18
|
+
fixed_prices: list[dict[str, Any]] | None = None,
|
|
19
|
+
) -> dict[str, Any]:
|
|
20
|
+
body: dict[str, Any] = {}
|
|
21
|
+
if name is not None:
|
|
22
|
+
body["name"] = name
|
|
23
|
+
if currency is not None:
|
|
24
|
+
body["currency"] = currency
|
|
25
|
+
if external_plan_id is not None:
|
|
26
|
+
body["externalPlanId"] = external_plan_id
|
|
27
|
+
if description is not None:
|
|
28
|
+
body["description"] = description
|
|
29
|
+
if list_price_cents is not None:
|
|
30
|
+
body["listPriceCents"] = list_price_cents
|
|
31
|
+
if tokens is not None:
|
|
32
|
+
body["tokens"] = tokens
|
|
33
|
+
if usage_based_prices is not None:
|
|
34
|
+
body["usageBasedPrices"] = usage_based_prices
|
|
35
|
+
if fixed_prices is not None:
|
|
36
|
+
body["fixedPrices"] = fixed_prices
|
|
37
|
+
return body
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class PlansResource:
|
|
41
|
+
def __init__(self, http: HttpClient) -> None:
|
|
42
|
+
self._http = http
|
|
43
|
+
|
|
44
|
+
def create(
|
|
45
|
+
self,
|
|
46
|
+
*,
|
|
47
|
+
name: str,
|
|
48
|
+
currency: str,
|
|
49
|
+
external_plan_id: str | None = None,
|
|
50
|
+
description: str | None = None,
|
|
51
|
+
list_price_cents: int | None = None,
|
|
52
|
+
tokens: int | None = None,
|
|
53
|
+
usage_based_prices: list[dict[str, Any]] | None = None,
|
|
54
|
+
fixed_prices: list[dict[str, Any]] | None = None,
|
|
55
|
+
) -> Plan:
|
|
56
|
+
body = _build_plan_body(
|
|
57
|
+
name=name,
|
|
58
|
+
currency=currency,
|
|
59
|
+
external_plan_id=external_plan_id,
|
|
60
|
+
description=description,
|
|
61
|
+
list_price_cents=list_price_cents,
|
|
62
|
+
tokens=tokens,
|
|
63
|
+
usage_based_prices=usage_based_prices,
|
|
64
|
+
fixed_prices=fixed_prices,
|
|
65
|
+
)
|
|
66
|
+
return Plan.from_dict(self._http.post("/api/plans", body))
|
|
67
|
+
|
|
68
|
+
def list(self) -> list[Plan]:
|
|
69
|
+
return [Plan.from_dict(p) for p in self._http.get("/api/plans")]
|
|
70
|
+
|
|
71
|
+
def get(self, plan_id: int) -> Plan:
|
|
72
|
+
return Plan.from_dict(self._http.get(f"/api/plans/{plan_id}"))
|
|
73
|
+
|
|
74
|
+
def update(
|
|
75
|
+
self,
|
|
76
|
+
plan_id: int,
|
|
77
|
+
*,
|
|
78
|
+
name: str,
|
|
79
|
+
currency: str,
|
|
80
|
+
external_plan_id: str | None = None,
|
|
81
|
+
description: str | None = None,
|
|
82
|
+
list_price_cents: int | None = None,
|
|
83
|
+
tokens: int | None = None,
|
|
84
|
+
usage_based_prices: list[dict[str, Any]] | None = None,
|
|
85
|
+
fixed_prices: list[dict[str, Any]] | None = None,
|
|
86
|
+
) -> Plan:
|
|
87
|
+
body = _build_plan_body(
|
|
88
|
+
name=name,
|
|
89
|
+
currency=currency,
|
|
90
|
+
external_plan_id=external_plan_id,
|
|
91
|
+
description=description,
|
|
92
|
+
list_price_cents=list_price_cents,
|
|
93
|
+
tokens=tokens,
|
|
94
|
+
usage_based_prices=usage_based_prices,
|
|
95
|
+
fixed_prices=fixed_prices,
|
|
96
|
+
)
|
|
97
|
+
return Plan.from_dict(self._http.put(f"/api/plans/{plan_id}", body))
|
|
98
|
+
|
|
99
|
+
def delete(self, plan_id: int) -> None:
|
|
100
|
+
self._http.delete(f"/api/plans/{plan_id}")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class AsyncPlansResource:
|
|
104
|
+
def __init__(self, http: AsyncHttpClient) -> None:
|
|
105
|
+
self._http = http
|
|
106
|
+
|
|
107
|
+
async def create(
|
|
108
|
+
self,
|
|
109
|
+
*,
|
|
110
|
+
name: str,
|
|
111
|
+
currency: str,
|
|
112
|
+
external_plan_id: str | None = None,
|
|
113
|
+
description: str | None = None,
|
|
114
|
+
list_price_cents: int | None = None,
|
|
115
|
+
tokens: int | None = None,
|
|
116
|
+
usage_based_prices: list[dict[str, Any]] | None = None,
|
|
117
|
+
fixed_prices: list[dict[str, Any]] | None = None,
|
|
118
|
+
) -> Plan:
|
|
119
|
+
body = _build_plan_body(
|
|
120
|
+
name=name,
|
|
121
|
+
currency=currency,
|
|
122
|
+
external_plan_id=external_plan_id,
|
|
123
|
+
description=description,
|
|
124
|
+
list_price_cents=list_price_cents,
|
|
125
|
+
tokens=tokens,
|
|
126
|
+
usage_based_prices=usage_based_prices,
|
|
127
|
+
fixed_prices=fixed_prices,
|
|
128
|
+
)
|
|
129
|
+
return Plan.from_dict(await self._http.post("/api/plans", body))
|
|
130
|
+
|
|
131
|
+
async def list(self) -> list[Plan]:
|
|
132
|
+
return [Plan.from_dict(p) for p in await self._http.get("/api/plans")]
|
|
133
|
+
|
|
134
|
+
async def get(self, plan_id: int) -> Plan:
|
|
135
|
+
return Plan.from_dict(await self._http.get(f"/api/plans/{plan_id}"))
|
|
136
|
+
|
|
137
|
+
async def update(
|
|
138
|
+
self,
|
|
139
|
+
plan_id: int,
|
|
140
|
+
*,
|
|
141
|
+
name: str,
|
|
142
|
+
currency: str,
|
|
143
|
+
external_plan_id: str | None = None,
|
|
144
|
+
description: str | None = None,
|
|
145
|
+
list_price_cents: int | None = None,
|
|
146
|
+
tokens: int | None = None,
|
|
147
|
+
usage_based_prices: list[dict[str, Any]] | None = None,
|
|
148
|
+
fixed_prices: list[dict[str, Any]] | None = None,
|
|
149
|
+
) -> Plan:
|
|
150
|
+
body = _build_plan_body(
|
|
151
|
+
name=name,
|
|
152
|
+
currency=currency,
|
|
153
|
+
external_plan_id=external_plan_id,
|
|
154
|
+
description=description,
|
|
155
|
+
list_price_cents=list_price_cents,
|
|
156
|
+
tokens=tokens,
|
|
157
|
+
usage_based_prices=usage_based_prices,
|
|
158
|
+
fixed_prices=fixed_prices,
|
|
159
|
+
)
|
|
160
|
+
return Plan.from_dict(await self._http.put(f"/api/plans/{plan_id}", body))
|
|
161
|
+
|
|
162
|
+
async def delete(self, plan_id: int) -> None:
|
|
163
|
+
await self._http.delete(f"/api/plans/{plan_id}")
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import Any
|
|
5
|
+
from urllib.parse import urlencode
|
|
6
|
+
|
|
7
|
+
from .._http import AsyncHttpClient, HttpClient
|
|
8
|
+
from .._models import Page, Pagination, Subscription
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _with_workspace(path: str, workspace_id: str | None) -> str:
|
|
12
|
+
if not workspace_id:
|
|
13
|
+
return path
|
|
14
|
+
sep = "&" if "?" in path else "?"
|
|
15
|
+
return f"{path}{sep}{urlencode({'workspaceId': workspace_id})}"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _resolve_workspace(http: HttpClient | AsyncHttpClient, workspace_id: str | None) -> str | None:
|
|
19
|
+
return workspace_id or http.workspace_id
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _parse_subscription_list(data: Any) -> Page[Subscription]:
|
|
23
|
+
if isinstance(data, dict) and "data" in data:
|
|
24
|
+
return Page(
|
|
25
|
+
data=[Subscription.from_dict(s) for s in data["data"]],
|
|
26
|
+
pagination=Pagination.from_dict(data["pagination"]),
|
|
27
|
+
)
|
|
28
|
+
items = data if isinstance(data, list) else [data]
|
|
29
|
+
return Page(
|
|
30
|
+
data=[Subscription.from_dict(s) for s in items],
|
|
31
|
+
pagination=Pagination(1, len(items), len(items), 1),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class SubscriptionsResource:
|
|
36
|
+
def __init__(self, http: HttpClient) -> None:
|
|
37
|
+
self._http = http
|
|
38
|
+
|
|
39
|
+
def list(
|
|
40
|
+
self,
|
|
41
|
+
*,
|
|
42
|
+
user_id: str | None = None,
|
|
43
|
+
workspace_id: str | None = None,
|
|
44
|
+
status: str | None = None,
|
|
45
|
+
page: int | None = None,
|
|
46
|
+
limit: int | None = None,
|
|
47
|
+
) -> Page[Subscription]:
|
|
48
|
+
ws = _resolve_workspace(self._http, workspace_id)
|
|
49
|
+
data = self._http.get(
|
|
50
|
+
"/api/subscriptions",
|
|
51
|
+
{"userId": user_id, "workspaceId": ws, "status": status, "page": page, "limit": limit},
|
|
52
|
+
)
|
|
53
|
+
return _parse_subscription_list(data)
|
|
54
|
+
|
|
55
|
+
def get(self, subscription_id: int, *, workspace_id: str | None = None) -> Subscription:
|
|
56
|
+
ws = _resolve_workspace(self._http, workspace_id)
|
|
57
|
+
return Subscription.from_dict(
|
|
58
|
+
self._http.get(_with_workspace(f"/api/subscriptions/{subscription_id}", ws))
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def create(
|
|
62
|
+
self,
|
|
63
|
+
*,
|
|
64
|
+
user_id: str,
|
|
65
|
+
price: int,
|
|
66
|
+
currency: str,
|
|
67
|
+
interval: str,
|
|
68
|
+
plan_id: int | None = None,
|
|
69
|
+
plan_name: str | None = None,
|
|
70
|
+
user_name: str | None = None,
|
|
71
|
+
user_email: str | None = None,
|
|
72
|
+
status: str = "Active",
|
|
73
|
+
subscription_start: str | None = None,
|
|
74
|
+
subscription_end: str | None = None,
|
|
75
|
+
source: str = "sdk",
|
|
76
|
+
workspace_id: str | None = None,
|
|
77
|
+
) -> Subscription:
|
|
78
|
+
ws = _resolve_workspace(self._http, workspace_id)
|
|
79
|
+
body = {k: v for k, v in {
|
|
80
|
+
"userId": user_id,
|
|
81
|
+
"userName": user_name,
|
|
82
|
+
"userEmail": user_email,
|
|
83
|
+
"planId": plan_id,
|
|
84
|
+
"planName": plan_name,
|
|
85
|
+
"status": status,
|
|
86
|
+
"subscriptionStart": subscription_start,
|
|
87
|
+
"subscriptionEnd": subscription_end,
|
|
88
|
+
"price": price,
|
|
89
|
+
"currency": currency,
|
|
90
|
+
"interval": interval,
|
|
91
|
+
"source": source,
|
|
92
|
+
}.items() if v is not None}
|
|
93
|
+
return Subscription.from_dict(
|
|
94
|
+
self._http.post(_with_workspace("/api/subscriptions", ws), body)
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def end(
|
|
98
|
+
self,
|
|
99
|
+
subscription_id: int,
|
|
100
|
+
*,
|
|
101
|
+
subscription_end: str | None = None,
|
|
102
|
+
workspace_id: str | None = None,
|
|
103
|
+
) -> Subscription:
|
|
104
|
+
ws = _resolve_workspace(self._http, workspace_id)
|
|
105
|
+
body = {"subscriptionEnd": subscription_end} if subscription_end else {}
|
|
106
|
+
return Subscription.from_dict(
|
|
107
|
+
self._http.post(_with_workspace(f"/api/subscriptions/{subscription_id}/end", ws), body)
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def update(
|
|
111
|
+
self,
|
|
112
|
+
subscription_id: int,
|
|
113
|
+
data: dict[str, Any],
|
|
114
|
+
*,
|
|
115
|
+
workspace_id: str | None = None,
|
|
116
|
+
) -> Subscription:
|
|
117
|
+
ws = _resolve_workspace(self._http, workspace_id)
|
|
118
|
+
return Subscription.from_dict(
|
|
119
|
+
self._http.patch(_with_workspace(f"/api/subscriptions/{subscription_id}", ws), data)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def cancel(self, subscription_id: int, *, workspace_id: str | None = None) -> Subscription:
|
|
123
|
+
now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
124
|
+
return self.end(subscription_id, subscription_end=now, workspace_id=workspace_id)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class AsyncSubscriptionsResource:
|
|
128
|
+
def __init__(self, http: AsyncHttpClient) -> None:
|
|
129
|
+
self._http = http
|
|
130
|
+
|
|
131
|
+
async def list(
|
|
132
|
+
self,
|
|
133
|
+
*,
|
|
134
|
+
user_id: str | None = None,
|
|
135
|
+
workspace_id: str | None = None,
|
|
136
|
+
status: str | None = None,
|
|
137
|
+
page: int | None = None,
|
|
138
|
+
limit: int | None = None,
|
|
139
|
+
) -> Page[Subscription]:
|
|
140
|
+
ws = _resolve_workspace(self._http, workspace_id)
|
|
141
|
+
data = await self._http.get(
|
|
142
|
+
"/api/subscriptions",
|
|
143
|
+
{"userId": user_id, "workspaceId": ws, "status": status, "page": page, "limit": limit},
|
|
144
|
+
)
|
|
145
|
+
return _parse_subscription_list(data)
|
|
146
|
+
|
|
147
|
+
async def get(self, subscription_id: int, *, workspace_id: str | None = None) -> Subscription:
|
|
148
|
+
ws = _resolve_workspace(self._http, workspace_id)
|
|
149
|
+
return Subscription.from_dict(
|
|
150
|
+
await self._http.get(_with_workspace(f"/api/subscriptions/{subscription_id}", ws))
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
async def create(
|
|
154
|
+
self,
|
|
155
|
+
*,
|
|
156
|
+
user_id: str,
|
|
157
|
+
price: int,
|
|
158
|
+
currency: str,
|
|
159
|
+
interval: str,
|
|
160
|
+
plan_id: int | None = None,
|
|
161
|
+
plan_name: str | None = None,
|
|
162
|
+
user_name: str | None = None,
|
|
163
|
+
user_email: str | None = None,
|
|
164
|
+
status: str = "Active",
|
|
165
|
+
subscription_start: str | None = None,
|
|
166
|
+
subscription_end: str | None = None,
|
|
167
|
+
source: str = "sdk",
|
|
168
|
+
workspace_id: str | None = None,
|
|
169
|
+
) -> Subscription:
|
|
170
|
+
ws = _resolve_workspace(self._http, workspace_id)
|
|
171
|
+
body = {k: v for k, v in {
|
|
172
|
+
"userId": user_id,
|
|
173
|
+
"userName": user_name,
|
|
174
|
+
"userEmail": user_email,
|
|
175
|
+
"planId": plan_id,
|
|
176
|
+
"planName": plan_name,
|
|
177
|
+
"status": status,
|
|
178
|
+
"subscriptionStart": subscription_start,
|
|
179
|
+
"subscriptionEnd": subscription_end,
|
|
180
|
+
"price": price,
|
|
181
|
+
"currency": currency,
|
|
182
|
+
"interval": interval,
|
|
183
|
+
"source": source,
|
|
184
|
+
}.items() if v is not None}
|
|
185
|
+
return Subscription.from_dict(
|
|
186
|
+
await self._http.post(_with_workspace("/api/subscriptions", ws), body)
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
async def end(
|
|
190
|
+
self,
|
|
191
|
+
subscription_id: int,
|
|
192
|
+
*,
|
|
193
|
+
subscription_end: str | None = None,
|
|
194
|
+
workspace_id: str | None = None,
|
|
195
|
+
) -> Subscription:
|
|
196
|
+
ws = _resolve_workspace(self._http, workspace_id)
|
|
197
|
+
body = {"subscriptionEnd": subscription_end} if subscription_end else {}
|
|
198
|
+
return Subscription.from_dict(
|
|
199
|
+
await self._http.post(_with_workspace(f"/api/subscriptions/{subscription_id}/end", ws), body)
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
async def update(
|
|
203
|
+
self,
|
|
204
|
+
subscription_id: int,
|
|
205
|
+
data: dict[str, Any],
|
|
206
|
+
*,
|
|
207
|
+
workspace_id: str | None = None,
|
|
208
|
+
) -> Subscription:
|
|
209
|
+
ws = _resolve_workspace(self._http, workspace_id)
|
|
210
|
+
return Subscription.from_dict(
|
|
211
|
+
await self._http.patch(_with_workspace(f"/api/subscriptions/{subscription_id}", ws), data)
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
async def cancel(self, subscription_id: int, *, workspace_id: str | None = None) -> Subscription:
|
|
215
|
+
now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
216
|
+
return await self.end(subscription_id, subscription_end=now, workspace_id=workspace_id)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import Any
|
|
5
|
+
from urllib.parse import urlencode
|
|
6
|
+
|
|
7
|
+
from .._http import AsyncHttpClient, HttpClient
|
|
8
|
+
from .._models import Page, Pagination, Usage
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _resolve_workspace(http: HttpClient | AsyncHttpClient, workspace_id: str | None) -> str | None:
|
|
12
|
+
return workspace_id or http.workspace_id
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _parse_usage_list(data: Any) -> Page[Usage]:
|
|
16
|
+
if isinstance(data, dict) and "data" in data:
|
|
17
|
+
return Page(
|
|
18
|
+
data=[Usage.from_dict(u) for u in data["data"]],
|
|
19
|
+
pagination=Pagination.from_dict(data["pagination"]),
|
|
20
|
+
)
|
|
21
|
+
items = data if isinstance(data, list) else [data]
|
|
22
|
+
return Page(data=[Usage.from_dict(u) for u in items], pagination=Pagination(1, len(items), len(items), 1))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class UsageResource:
|
|
26
|
+
def __init__(self, http: HttpClient) -> None:
|
|
27
|
+
self._http = http
|
|
28
|
+
|
|
29
|
+
def report(
|
|
30
|
+
self,
|
|
31
|
+
*,
|
|
32
|
+
user_id: str,
|
|
33
|
+
input_tokens: int,
|
|
34
|
+
output_tokens: int,
|
|
35
|
+
provider: str,
|
|
36
|
+
model: str,
|
|
37
|
+
plan_id: int | None = None,
|
|
38
|
+
feature: str | None = None,
|
|
39
|
+
feature_session: str | None = None,
|
|
40
|
+
ts: int | None = None,
|
|
41
|
+
workspace_id: str | None = None,
|
|
42
|
+
) -> Usage:
|
|
43
|
+
ws = _resolve_workspace(self._http, workspace_id)
|
|
44
|
+
path = "/api/usages"
|
|
45
|
+
if ws:
|
|
46
|
+
path = f"{path}?{urlencode({'workspaceId': ws})}"
|
|
47
|
+
|
|
48
|
+
body: dict[str, Any] = {
|
|
49
|
+
"userId": user_id,
|
|
50
|
+
"inputTokens": input_tokens,
|
|
51
|
+
"outputTokens": output_tokens,
|
|
52
|
+
"llmProvider": provider,
|
|
53
|
+
"llmModel": model,
|
|
54
|
+
"ts": ts if ts is not None else int(time.time() * 1000),
|
|
55
|
+
}
|
|
56
|
+
if plan_id is not None:
|
|
57
|
+
body["planId"] = plan_id
|
|
58
|
+
if feature is not None:
|
|
59
|
+
body["feature"] = feature
|
|
60
|
+
if feature_session is not None:
|
|
61
|
+
body["featureSession"] = feature_session
|
|
62
|
+
|
|
63
|
+
return Usage.from_dict(self._http.post(path, body))
|
|
64
|
+
|
|
65
|
+
def list(
|
|
66
|
+
self,
|
|
67
|
+
*,
|
|
68
|
+
user_id: str | None = None,
|
|
69
|
+
workspace_id: str | None = None,
|
|
70
|
+
page: int | None = None,
|
|
71
|
+
limit: int | None = None,
|
|
72
|
+
) -> Page[Usage]:
|
|
73
|
+
ws = _resolve_workspace(self._http, workspace_id)
|
|
74
|
+
return _parse_usage_list(
|
|
75
|
+
self._http.get("/api/usages", {"userId": user_id, "workspaceId": ws, "page": page, "limit": limit})
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class AsyncUsageResource:
|
|
80
|
+
def __init__(self, http: AsyncHttpClient) -> None:
|
|
81
|
+
self._http = http
|
|
82
|
+
|
|
83
|
+
async def report(
|
|
84
|
+
self,
|
|
85
|
+
*,
|
|
86
|
+
user_id: str,
|
|
87
|
+
input_tokens: int,
|
|
88
|
+
output_tokens: int,
|
|
89
|
+
provider: str,
|
|
90
|
+
model: str,
|
|
91
|
+
plan_id: int | None = None,
|
|
92
|
+
feature: str | None = None,
|
|
93
|
+
feature_session: str | None = None,
|
|
94
|
+
ts: int | None = None,
|
|
95
|
+
workspace_id: str | None = None,
|
|
96
|
+
) -> Usage:
|
|
97
|
+
ws = _resolve_workspace(self._http, workspace_id)
|
|
98
|
+
path = "/api/usages"
|
|
99
|
+
if ws:
|
|
100
|
+
path = f"{path}?{urlencode({'workspaceId': ws})}"
|
|
101
|
+
|
|
102
|
+
body: dict[str, Any] = {
|
|
103
|
+
"userId": user_id,
|
|
104
|
+
"inputTokens": input_tokens,
|
|
105
|
+
"outputTokens": output_tokens,
|
|
106
|
+
"llmProvider": provider,
|
|
107
|
+
"llmModel": model,
|
|
108
|
+
"ts": ts if ts is not None else int(time.time() * 1000),
|
|
109
|
+
}
|
|
110
|
+
if plan_id is not None:
|
|
111
|
+
body["planId"] = plan_id
|
|
112
|
+
if feature is not None:
|
|
113
|
+
body["feature"] = feature
|
|
114
|
+
if feature_session is not None:
|
|
115
|
+
body["featureSession"] = feature_session
|
|
116
|
+
|
|
117
|
+
return Usage.from_dict(await self._http.post(path, body))
|
|
118
|
+
|
|
119
|
+
async def list(
|
|
120
|
+
self,
|
|
121
|
+
*,
|
|
122
|
+
user_id: str | None = None,
|
|
123
|
+
workspace_id: str | None = None,
|
|
124
|
+
page: int | None = None,
|
|
125
|
+
limit: int | None = None,
|
|
126
|
+
) -> Page[Usage]:
|
|
127
|
+
ws = _resolve_workspace(self._http, workspace_id)
|
|
128
|
+
return _parse_usage_list(
|
|
129
|
+
await self._http.get("/api/usages", {"userId": user_id, "workspaceId": ws, "page": page, "limit": limit})
|
|
130
|
+
)
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import Any
|
|
5
|
+
from urllib.parse import quote
|
|
6
|
+
|
|
7
|
+
from .._http import AsyncHttpClient, HttpClient
|
|
8
|
+
from .._models import Page, Pagination, User
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _build_user_body(
|
|
12
|
+
*,
|
|
13
|
+
user_id: str | None = None,
|
|
14
|
+
user_name: str | None = None,
|
|
15
|
+
user_email: str | None = None,
|
|
16
|
+
plan_name: str | None = None,
|
|
17
|
+
plan_id: int | None = None,
|
|
18
|
+
price: int | None = None,
|
|
19
|
+
currency: str | None = None,
|
|
20
|
+
interval: str | None = None,
|
|
21
|
+
subscription_date: str | None = None,
|
|
22
|
+
subscription_end_date: str | None = None,
|
|
23
|
+
source: str | None = None,
|
|
24
|
+
) -> dict[str, Any]:
|
|
25
|
+
body: dict[str, Any] = {}
|
|
26
|
+
if user_id is not None:
|
|
27
|
+
body["userId"] = user_id
|
|
28
|
+
if user_name is not None:
|
|
29
|
+
body["userName"] = user_name
|
|
30
|
+
if user_email is not None:
|
|
31
|
+
body["userEmail"] = user_email
|
|
32
|
+
if plan_name is not None:
|
|
33
|
+
body["planName"] = plan_name
|
|
34
|
+
if plan_id is not None:
|
|
35
|
+
body["planId"] = plan_id
|
|
36
|
+
if price is not None:
|
|
37
|
+
body["price"] = price
|
|
38
|
+
if currency is not None:
|
|
39
|
+
body["currency"] = currency
|
|
40
|
+
if interval is not None:
|
|
41
|
+
body["interval"] = interval
|
|
42
|
+
if subscription_date is not None:
|
|
43
|
+
body["subscriptionDate"] = subscription_date
|
|
44
|
+
if subscription_end_date is not None:
|
|
45
|
+
body["subscriptionEndDate"] = subscription_end_date
|
|
46
|
+
if source is not None:
|
|
47
|
+
body["source"] = source
|
|
48
|
+
return body
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _parse_user_response(data: Any) -> User | Page[User]:
|
|
52
|
+
if isinstance(data, dict) and "data" in data and "pagination" in data:
|
|
53
|
+
return Page(
|
|
54
|
+
data=[User.from_dict(u) for u in data["data"]],
|
|
55
|
+
pagination=Pagination.from_dict(data["pagination"]),
|
|
56
|
+
)
|
|
57
|
+
if isinstance(data, list):
|
|
58
|
+
return Page(data=[User.from_dict(u) for u in data], pagination=Pagination(1, len(data), len(data), 1))
|
|
59
|
+
return User.from_dict(data)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class UsersResource:
|
|
63
|
+
def __init__(self, http: HttpClient) -> None:
|
|
64
|
+
self._http = http
|
|
65
|
+
|
|
66
|
+
def create(
|
|
67
|
+
self,
|
|
68
|
+
*,
|
|
69
|
+
user_id: str,
|
|
70
|
+
plan_name: str,
|
|
71
|
+
price: int,
|
|
72
|
+
currency: str,
|
|
73
|
+
interval: str,
|
|
74
|
+
user_name: str | None = None,
|
|
75
|
+
user_email: str | None = None,
|
|
76
|
+
plan_id: int | None = None,
|
|
77
|
+
subscription_date: str | None = None,
|
|
78
|
+
source: str = "sdk",
|
|
79
|
+
) -> User:
|
|
80
|
+
body = _build_user_body(
|
|
81
|
+
user_id=user_id,
|
|
82
|
+
user_name=user_name,
|
|
83
|
+
user_email=user_email,
|
|
84
|
+
plan_name=plan_name,
|
|
85
|
+
plan_id=plan_id,
|
|
86
|
+
price=price,
|
|
87
|
+
currency=currency,
|
|
88
|
+
interval=interval,
|
|
89
|
+
subscription_date=subscription_date,
|
|
90
|
+
source=source,
|
|
91
|
+
)
|
|
92
|
+
body.setdefault("subscriptionDate", time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()))
|
|
93
|
+
return User.from_dict(self._http.post("/api/users", body))
|
|
94
|
+
|
|
95
|
+
def list(
|
|
96
|
+
self,
|
|
97
|
+
*,
|
|
98
|
+
page: int | None = None,
|
|
99
|
+
limit: int | None = None,
|
|
100
|
+
plan_name: str | None = None,
|
|
101
|
+
) -> Page[User]:
|
|
102
|
+
data = self._http.get("/api/users", {"page": page, "limit": limit, "planName": plan_name})
|
|
103
|
+
result = _parse_user_response(data)
|
|
104
|
+
return result if isinstance(result, Page) else Page(data=[result], pagination=Pagination(1, 1, 1, 1))
|
|
105
|
+
|
|
106
|
+
def get(self, user_id: str) -> User:
|
|
107
|
+
return User.from_dict(self._http.get(f"/api/users/external/{quote(user_id, safe='')}"))
|
|
108
|
+
|
|
109
|
+
def update(
|
|
110
|
+
self,
|
|
111
|
+
user_id: str,
|
|
112
|
+
*,
|
|
113
|
+
user_name: str | None = None,
|
|
114
|
+
user_email: str | None = None,
|
|
115
|
+
plan_name: str | None = None,
|
|
116
|
+
plan_id: int | None = None,
|
|
117
|
+
price: int | None = None,
|
|
118
|
+
currency: str | None = None,
|
|
119
|
+
interval: str | None = None,
|
|
120
|
+
subscription_date: str | None = None,
|
|
121
|
+
subscription_end_date: str | None = None,
|
|
122
|
+
source: str | None = None,
|
|
123
|
+
) -> User:
|
|
124
|
+
body = _build_user_body(
|
|
125
|
+
user_name=user_name,
|
|
126
|
+
user_email=user_email,
|
|
127
|
+
plan_name=plan_name,
|
|
128
|
+
plan_id=plan_id,
|
|
129
|
+
price=price,
|
|
130
|
+
currency=currency,
|
|
131
|
+
interval=interval,
|
|
132
|
+
subscription_date=subscription_date,
|
|
133
|
+
subscription_end_date=subscription_end_date,
|
|
134
|
+
source=source,
|
|
135
|
+
)
|
|
136
|
+
return User.from_dict(self._http.put(f"/api/users/external/{quote(user_id, safe='')}", body))
|
|
137
|
+
|
|
138
|
+
def delete(self, user_id: str) -> None:
|
|
139
|
+
self._http.delete(f"/api/users/external/{quote(user_id, safe='')}")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class AsyncUsersResource:
|
|
143
|
+
def __init__(self, http: AsyncHttpClient) -> None:
|
|
144
|
+
self._http = http
|
|
145
|
+
|
|
146
|
+
async def create(
|
|
147
|
+
self,
|
|
148
|
+
*,
|
|
149
|
+
user_id: str,
|
|
150
|
+
plan_name: str,
|
|
151
|
+
price: int,
|
|
152
|
+
currency: str,
|
|
153
|
+
interval: str,
|
|
154
|
+
user_name: str | None = None,
|
|
155
|
+
user_email: str | None = None,
|
|
156
|
+
plan_id: int | None = None,
|
|
157
|
+
subscription_date: str | None = None,
|
|
158
|
+
source: str = "sdk",
|
|
159
|
+
) -> User:
|
|
160
|
+
body = _build_user_body(
|
|
161
|
+
user_id=user_id,
|
|
162
|
+
user_name=user_name,
|
|
163
|
+
user_email=user_email,
|
|
164
|
+
plan_name=plan_name,
|
|
165
|
+
plan_id=plan_id,
|
|
166
|
+
price=price,
|
|
167
|
+
currency=currency,
|
|
168
|
+
interval=interval,
|
|
169
|
+
subscription_date=subscription_date,
|
|
170
|
+
source=source,
|
|
171
|
+
)
|
|
172
|
+
body.setdefault("subscriptionDate", time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()))
|
|
173
|
+
return User.from_dict(await self._http.post("/api/users", body))
|
|
174
|
+
|
|
175
|
+
async def list(
|
|
176
|
+
self,
|
|
177
|
+
*,
|
|
178
|
+
page: int | None = None,
|
|
179
|
+
limit: int | None = None,
|
|
180
|
+
plan_name: str | None = None,
|
|
181
|
+
) -> Page[User]:
|
|
182
|
+
data = await self._http.get("/api/users", {"page": page, "limit": limit, "planName": plan_name})
|
|
183
|
+
result = _parse_user_response(data)
|
|
184
|
+
return result if isinstance(result, Page) else Page(data=[result], pagination=Pagination(1, 1, 1, 1))
|
|
185
|
+
|
|
186
|
+
async def get(self, user_id: str) -> User:
|
|
187
|
+
return User.from_dict(await self._http.get(f"/api/users/external/{quote(user_id, safe='')}"))
|
|
188
|
+
|
|
189
|
+
async def update(
|
|
190
|
+
self,
|
|
191
|
+
user_id: str,
|
|
192
|
+
*,
|
|
193
|
+
user_name: str | None = None,
|
|
194
|
+
user_email: str | None = None,
|
|
195
|
+
plan_name: str | None = None,
|
|
196
|
+
plan_id: int | None = None,
|
|
197
|
+
price: int | None = None,
|
|
198
|
+
currency: str | None = None,
|
|
199
|
+
interval: str | None = None,
|
|
200
|
+
subscription_date: str | None = None,
|
|
201
|
+
subscription_end_date: str | None = None,
|
|
202
|
+
source: str | None = None,
|
|
203
|
+
) -> User:
|
|
204
|
+
body = _build_user_body(
|
|
205
|
+
user_name=user_name,
|
|
206
|
+
user_email=user_email,
|
|
207
|
+
plan_name=plan_name,
|
|
208
|
+
plan_id=plan_id,
|
|
209
|
+
price=price,
|
|
210
|
+
currency=currency,
|
|
211
|
+
interval=interval,
|
|
212
|
+
subscription_date=subscription_date,
|
|
213
|
+
subscription_end_date=subscription_end_date,
|
|
214
|
+
source=source,
|
|
215
|
+
)
|
|
216
|
+
return User.from_dict(await self._http.put(f"/api/users/external/{quote(user_id, safe='')}", body))
|
|
217
|
+
|
|
218
|
+
async def delete(self, user_id: str) -> None:
|
|
219
|
+
await self._http.delete(f"/api/users/external/{quote(user_id, safe='')}")
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: paiment
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Official Python SDK for the Paiment billing API
|
|
5
|
+
Author: Paiment
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://paiment.tech
|
|
8
|
+
Project-URL: Documentation, https://paiment.tech
|
|
9
|
+
Keywords: paiment,billing,sdk,llm,usage
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: httpx>=0.27.0
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
24
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
25
|
+
Requires-Dist: mypy>=1.9; extra == "dev"
|
|
26
|
+
Requires-Dist: build; extra == "dev"
|
|
27
|
+
Requires-Dist: twine; extra == "dev"
|
|
28
|
+
|
|
29
|
+
# paiment (Python SDK)
|
|
30
|
+
|
|
31
|
+
Official Python SDK for the [Paiment](https://paiment.tech) billing API.
|
|
32
|
+
|
|
33
|
+
> **npm users:** install `@paiment/sdk` instead. This package is the Python equivalent.
|
|
34
|
+
|
|
35
|
+
Use it in your **backend** to register subscribers, report LLM usage, and manage plans.
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install paiment
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Python 3.9+.
|
|
44
|
+
|
|
45
|
+
## Setup
|
|
46
|
+
|
|
47
|
+
Get an API key from **Settings → API Keys**.
|
|
48
|
+
|
|
49
|
+
| Key format | Notes |
|
|
50
|
+
|------------|--------|
|
|
51
|
+
| `tenantId:workspaceId:secret` | **Recommended** |
|
|
52
|
+
| `tenantId:secret` | Pass `workspace_id` on the client |
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from paiment import PaimentClient
|
|
56
|
+
|
|
57
|
+
client = PaimentClient(api_key="tenantId:workspaceId:secret")
|
|
58
|
+
# client = PaimentClient(api_key="...", base_url="http://localhost:5246")
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Quick start
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from paiment import PaimentClient
|
|
65
|
+
|
|
66
|
+
with PaimentClient(api_key="tenantId:workspaceId:secret") as client:
|
|
67
|
+
sub = client.subscriptions.create(
|
|
68
|
+
user_id="user_123",
|
|
69
|
+
plan_name="Pro",
|
|
70
|
+
price=2999,
|
|
71
|
+
currency="USD",
|
|
72
|
+
interval="monthly",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
client.usage.report(
|
|
76
|
+
user_id="user_123",
|
|
77
|
+
input_tokens=1250,
|
|
78
|
+
output_tokens=340,
|
|
79
|
+
provider="openai",
|
|
80
|
+
model="gpt-4o",
|
|
81
|
+
plan_id=sub.plan_id,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
client.subscriptions.end(sub.id)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Async
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from paiment import AsyncPaimentClient
|
|
91
|
+
|
|
92
|
+
async with AsyncPaimentClient(api_key="tenantId:workspaceId:secret") as client:
|
|
93
|
+
sub = await client.subscriptions.create(
|
|
94
|
+
user_id="user_123",
|
|
95
|
+
plan_name="Pro",
|
|
96
|
+
price=2999,
|
|
97
|
+
currency="USD",
|
|
98
|
+
interval="monthly",
|
|
99
|
+
)
|
|
100
|
+
await client.usage.report(
|
|
101
|
+
user_id="user_123",
|
|
102
|
+
input_tokens=1250,
|
|
103
|
+
output_tokens=340,
|
|
104
|
+
provider="openai",
|
|
105
|
+
model="gpt-4o",
|
|
106
|
+
)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## API reference
|
|
110
|
+
|
|
111
|
+
### Subscriptions
|
|
112
|
+
|
|
113
|
+
| Method | Description |
|
|
114
|
+
|--------|-------------|
|
|
115
|
+
| `create(user_id, plan_name, price, currency, interval, ...)` | New subscription |
|
|
116
|
+
| `list(user_id=..., status=...)` | List subscriptions |
|
|
117
|
+
| `get(subscription_id)` | Get by id |
|
|
118
|
+
| `update(subscription_id, data)` | Patch subscription |
|
|
119
|
+
| `end(subscription_id)` | Close subscription |
|
|
120
|
+
| `cancel(subscription_id)` | End immediately |
|
|
121
|
+
|
|
122
|
+
### Users
|
|
123
|
+
|
|
124
|
+
| Method | Description |
|
|
125
|
+
|--------|-------------|
|
|
126
|
+
| `create(user_id, plan_name, price, currency, interval, ...)` | User + subscription shortcut |
|
|
127
|
+
| `get(user_id)` | Get by your user id |
|
|
128
|
+
| `list(plan_name=..., page=..., limit=...)` | List users |
|
|
129
|
+
| `update(user_id, ...)` | Partial update |
|
|
130
|
+
| `delete(user_id)` | Delete user |
|
|
131
|
+
|
|
132
|
+
### Usage
|
|
133
|
+
|
|
134
|
+
| Method | Description |
|
|
135
|
+
|--------|-------------|
|
|
136
|
+
| `report(user_id, input_tokens, output_tokens, provider, model, ...)` | Record usage |
|
|
137
|
+
| `list(user_id=..., page=..., limit=...)` | List usage rows |
|
|
138
|
+
|
|
139
|
+
### Plans
|
|
140
|
+
|
|
141
|
+
| Method | Description |
|
|
142
|
+
|--------|-------------|
|
|
143
|
+
| `create(name, currency, list_price_cents=..., ...)` | Create plan |
|
|
144
|
+
| `list()` / `get(id)` / `update(id, ...)` / `delete(id)` | CRUD |
|
|
145
|
+
|
|
146
|
+
## Fields
|
|
147
|
+
|
|
148
|
+
| Field | Meaning |
|
|
149
|
+
|-------|---------|
|
|
150
|
+
| `user_id` | Your identifier for the end-user |
|
|
151
|
+
| `plan_name` | Plan catalog name |
|
|
152
|
+
| `price` | Subscription price in **cents** |
|
|
153
|
+
| `list_price_cents` | Catalog list price on plans only |
|
|
154
|
+
|
|
155
|
+
## Errors
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
from paiment import PaimentAuthError, PaimentNotFoundError, PaimentValidationError, PaimentError
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
| Class | HTTP status |
|
|
162
|
+
|-------|-------------|
|
|
163
|
+
| `PaimentAuthError` | 401 |
|
|
164
|
+
| `PaimentNotFoundError` | 404 |
|
|
165
|
+
| `PaimentValidationError` | 400 |
|
|
166
|
+
| `PaimentError` | other |
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
paiment/__init__.py,sha256=CFxl3p38owsvaL1Np57Pp5bdMm6VF4QsV_NH6EVqauw,2333
|
|
2
|
+
paiment/_errors.py,sha256=CU0JTGvp1odMPdXed9FSSalwCJyV6SEFYOhc5nnc4Ec,772
|
|
3
|
+
paiment/_http.py,sha256=VVFJKvLHGkndiH0zBxoJLn76DT0gYv8UY1j4Owho40U,4028
|
|
4
|
+
paiment/_models.py,sha256=TNl4Xo0cRxecWUW7fHvyCTdkbTKe1KjKF1sjxz2MMwk,6511
|
|
5
|
+
paiment/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
paiment/resources/plans.py,sha256=d0NXso9Q_11MJXwe0yxuibNGL_ha3mjE0eFPmmO9Ep8,5310
|
|
7
|
+
paiment/resources/subscriptions.py,sha256=CCknfCZCIv_TssMjJG1JF6mJ-HZol5v2wB0bkyi90h4,7514
|
|
8
|
+
paiment/resources/usage.py,sha256=u2Ucb02gkJykEFYkTi-ntG63nGPEolnzhDKQNAOV-ak,4082
|
|
9
|
+
paiment/resources/users.py,sha256=uc3dhIrK-tGTueLghpf5R8aNZdogUeHn8GJN52i1kkE,7227
|
|
10
|
+
paiment-0.2.0.dist-info/METADATA,sha256=gvVRcXuu6EtEwx7F2lkugcQo1Rz3n7P44Ib-FV2QuQY,4540
|
|
11
|
+
paiment-0.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
12
|
+
paiment-0.2.0.dist-info/top_level.txt,sha256=GtbfxyFcWDyErOY1gORz2Tz1us6Gi8AUKOP5LlZ4uRk,8
|
|
13
|
+
paiment-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
paiment
|