sub2api 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.
- sub2api-0.1.0/.gitignore +11 -0
- sub2api-0.1.0/LICENSE +21 -0
- sub2api-0.1.0/PKG-INFO +249 -0
- sub2api-0.1.0/README.md +200 -0
- sub2api-0.1.0/pyproject.toml +60 -0
- sub2api-0.1.0/src/sub2api/__init__.py +61 -0
- sub2api-0.1.0/src/sub2api/_client.py +486 -0
- sub2api-0.1.0/src/sub2api/_exceptions.py +83 -0
- sub2api-0.1.0/src/sub2api/_models.py +194 -0
- sub2api-0.1.0/src/sub2api/_resources.py +574 -0
- sub2api-0.1.0/src/sub2api/py.typed +0 -0
- sub2api-0.1.0/tests/test_client.py +308 -0
- sub2api-0.1.0/tests/test_resources.py +226 -0
sub2api-0.1.0/.gitignore
ADDED
sub2api-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Eight Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
sub2api-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: sub2api
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for the user-facing API of Sub2API instances
|
|
5
|
+
Author: Eight Labs
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Eight Labs
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Keywords: ai-gateway,api,sdk,sub2api
|
|
29
|
+
Classifier: Development Status :: 3 - Alpha
|
|
30
|
+
Classifier: Intended Audience :: Developers
|
|
31
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
32
|
+
Classifier: Programming Language :: Python :: 3
|
|
33
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
34
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
37
|
+
Classifier: Typing :: Typed
|
|
38
|
+
Requires-Python: >=3.10
|
|
39
|
+
Requires-Dist: curl-cffi<1,>=0.10
|
|
40
|
+
Provides-Extra: dev
|
|
41
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
42
|
+
Requires-Dist: httpx<1,>=0.27; extra == 'dev'
|
|
43
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
44
|
+
Requires-Dist: pytest-cov>=5; extra == 'dev'
|
|
45
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
46
|
+
Requires-Dist: ruff>=0.7; extra == 'dev'
|
|
47
|
+
Requires-Dist: twine>=5; extra == 'dev'
|
|
48
|
+
Description-Content-Type: text/markdown
|
|
49
|
+
|
|
50
|
+
# sub2api
|
|
51
|
+
|
|
52
|
+
`sub2api` is a Python client for the shared user-facing panel API exposed by Sub2API instances. One client object represents one user's in-memory dashboard session. Requests use `curl_cffi` with Chrome browser impersonation by default.
|
|
53
|
+
|
|
54
|
+
The library targets operations present on standard Sub2API deployments: account balance, platform quotas, usage history and statistics, API keys, groups, subscriptions, announcements, and redemption.
|
|
55
|
+
|
|
56
|
+
## Install
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install sub2api
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Python 3.10 or newer is required.
|
|
63
|
+
|
|
64
|
+
## Authenticate with an existing session
|
|
65
|
+
|
|
66
|
+
The dashboard's access token is different from an `sk-...` gateway API key. Browser deployments normally store the panel tokens under `auth_token` and `refresh_token` in local storage.
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
import os
|
|
70
|
+
|
|
71
|
+
from sub2api import Sub2API
|
|
72
|
+
|
|
73
|
+
client = Sub2API(
|
|
74
|
+
"https://sub2api.example.com",
|
|
75
|
+
access_token=os.environ["SUB2API_ACCESS_TOKEN"],
|
|
76
|
+
refresh_token=os.environ.get("SUB2API_REFRESH_TOKEN"),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
print(client.me().email)
|
|
80
|
+
print(client.balance().balance)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Pass either the instance origin or its full `/api/v1` URL. Tokens are retained only in memory. If a refresh token is supplied, the client rotates the token pair after an authenticated `401`. Supplying `expires_at` as a Unix timestamp also enables proactive refresh.
|
|
84
|
+
|
|
85
|
+
The default browser fingerprint is Chrome. Choose another `curl_cffi` fingerprint or configure proxies by supplying your own `curl_cffi.requests.Session`:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from curl_cffi import requests
|
|
89
|
+
|
|
90
|
+
session = requests.Session(impersonate="safari")
|
|
91
|
+
client = Sub2API("https://sub2api.example.com", session=session)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Log in with email and password
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from sub2api import Sub2API
|
|
98
|
+
|
|
99
|
+
with Sub2API("https://sub2api.example.com") as client:
|
|
100
|
+
user = client.login("person@example.com", "password")
|
|
101
|
+
print(user.username)
|
|
102
|
+
print(client.is_authenticated)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
An instance with CAPTCHA enabled requires the corresponding proof:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
client.login(
|
|
109
|
+
"person@example.com",
|
|
110
|
+
"password",
|
|
111
|
+
turnstile_token="captcha-proof",
|
|
112
|
+
)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
For a TOTP-enabled account, `login()` raises `TwoFactorRequired` and retains the temporary challenge in memory:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from sub2api import Sub2API, TwoFactorRequired
|
|
119
|
+
|
|
120
|
+
client = Sub2API("https://sub2api.example.com")
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
client.login("person@example.com", "password")
|
|
124
|
+
except TwoFactorRequired:
|
|
125
|
+
client.complete_2fa("123456")
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Common operations
|
|
129
|
+
|
|
130
|
+
Resources are callable for their common list operation and also expose explicit methods.
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
balance = client.balance()
|
|
134
|
+
quotas = client.account.platform_quotas()
|
|
135
|
+
|
|
136
|
+
groups = client.groups()
|
|
137
|
+
group_rates = client.groups.rates()
|
|
138
|
+
|
|
139
|
+
first_page = client.keys(page_size=50, status="active")
|
|
140
|
+
for api_key in first_page:
|
|
141
|
+
print(api_key.id, api_key.name, api_key.group.name)
|
|
142
|
+
|
|
143
|
+
all_keys = client.keys.all()
|
|
144
|
+
resolved = client.keys.with_group_multipliers()
|
|
145
|
+
for item in resolved:
|
|
146
|
+
print(
|
|
147
|
+
item.api_key.key,
|
|
148
|
+
item.group_id,
|
|
149
|
+
item.base_multiplier,
|
|
150
|
+
item.custom_multiplier,
|
|
151
|
+
item.effective_multiplier,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
multiplier_by_key = client.keys.multiplier_map(key_by="key")
|
|
155
|
+
multiplier_by_id = client.keys.multiplier_map(key_by="id")
|
|
156
|
+
|
|
157
|
+
created = client.keys.create("automation", group_id=groups[0].id)
|
|
158
|
+
client.keys.update(created.id, name="nightly automation")
|
|
159
|
+
client.keys.set_status(created.id, active=False)
|
|
160
|
+
client.keys.delete(created.id)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
`all()` follows pagination until every key has been fetched. `with_group_multipliers()` joins each key to its group and reports the base, user-specific, and effective rate; the user-specific rate from `/groups/rates` takes precedence. `multiplier_map()` returns the effective rate keyed by the API key value, key ID, or name. Name collisions raise an error instead of silently overwriting an entry.
|
|
164
|
+
|
|
165
|
+
API key values are available through `api_key.key`, but object representations redact fields that commonly contain credentials.
|
|
166
|
+
|
|
167
|
+
## Usage history
|
|
168
|
+
|
|
169
|
+
`history` and `usage` refer to the same resource.
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
from datetime import date, timedelta
|
|
173
|
+
|
|
174
|
+
end = date.today()
|
|
175
|
+
start = end - timedelta(days=7)
|
|
176
|
+
|
|
177
|
+
page = client.history(
|
|
178
|
+
start_date=start,
|
|
179
|
+
end_date=end,
|
|
180
|
+
page_size=100,
|
|
181
|
+
sort_by="created_at",
|
|
182
|
+
sort_order="desc",
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
for record in page:
|
|
186
|
+
print(record.created_at, record.model, record.actual_cost)
|
|
187
|
+
|
|
188
|
+
for record in client.history.iter(page_size=100):
|
|
189
|
+
process(record)
|
|
190
|
+
|
|
191
|
+
stats = client.usage.stats(start_date=start, end_date=end)
|
|
192
|
+
dashboard = client.usage.dashboard()
|
|
193
|
+
trend = client.usage.trend(start_date=start, end_date=end, granularity="day")
|
|
194
|
+
models = client.usage.models(start_date=start, end_date=end)
|
|
195
|
+
snapshot = client.usage.snapshot(start_date=start, end_date=end)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Other shared resources
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
active_subscriptions = client.subscriptions(active=True)
|
|
202
|
+
announcements = client.announcements()
|
|
203
|
+
client.announcements.mark_read(announcements[0].id)
|
|
204
|
+
|
|
205
|
+
result = client.redeem("REDEMPTION-CODE")
|
|
206
|
+
redemption_history = client.redeem.history()
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Fork-specific endpoints
|
|
210
|
+
|
|
211
|
+
`request()` provides the same authentication, envelope handling, timezone parameter, refresh behavior, and error mapping for relative endpoints that are not part of the stable resource API.
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
result = client.request("GET", "some-fork-specific-endpoint")
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Absolute URLs and parent-path traversal are rejected so a session token cannot be redirected outside the configured API root.
|
|
218
|
+
|
|
219
|
+
## Errors
|
|
220
|
+
|
|
221
|
+
HTTP and Sub2API envelope failures use typed exceptions:
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
from sub2api import AuthenticationError, RateLimitError, Sub2APIError
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
client.keys.create("automation")
|
|
228
|
+
except RateLimitError as error:
|
|
229
|
+
print(error.retry_after)
|
|
230
|
+
except AuthenticationError:
|
|
231
|
+
client.login("person@example.com", "password")
|
|
232
|
+
except Sub2APIError as error:
|
|
233
|
+
print(error)
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Remote plaintext HTTP is rejected by default because it exposes login credentials and tokens. Localhost HTTP is allowed for development; other HTTP instances require `allow_insecure=True`.
|
|
237
|
+
|
|
238
|
+
## Development
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
python -m pip install -e '.[dev]'
|
|
242
|
+
pytest
|
|
243
|
+
ruff check .
|
|
244
|
+
mypy
|
|
245
|
+
python -m build
|
|
246
|
+
twine check dist/*
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
HAR captures are ignored by Git because they can contain live session credentials.
|
sub2api-0.1.0/README.md
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# sub2api
|
|
2
|
+
|
|
3
|
+
`sub2api` is a Python client for the shared user-facing panel API exposed by Sub2API instances. One client object represents one user's in-memory dashboard session. Requests use `curl_cffi` with Chrome browser impersonation by default.
|
|
4
|
+
|
|
5
|
+
The library targets operations present on standard Sub2API deployments: account balance, platform quotas, usage history and statistics, API keys, groups, subscriptions, announcements, and redemption.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install sub2api
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Python 3.10 or newer is required.
|
|
14
|
+
|
|
15
|
+
## Authenticate with an existing session
|
|
16
|
+
|
|
17
|
+
The dashboard's access token is different from an `sk-...` gateway API key. Browser deployments normally store the panel tokens under `auth_token` and `refresh_token` in local storage.
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
import os
|
|
21
|
+
|
|
22
|
+
from sub2api import Sub2API
|
|
23
|
+
|
|
24
|
+
client = Sub2API(
|
|
25
|
+
"https://sub2api.example.com",
|
|
26
|
+
access_token=os.environ["SUB2API_ACCESS_TOKEN"],
|
|
27
|
+
refresh_token=os.environ.get("SUB2API_REFRESH_TOKEN"),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
print(client.me().email)
|
|
31
|
+
print(client.balance().balance)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Pass either the instance origin or its full `/api/v1` URL. Tokens are retained only in memory. If a refresh token is supplied, the client rotates the token pair after an authenticated `401`. Supplying `expires_at` as a Unix timestamp also enables proactive refresh.
|
|
35
|
+
|
|
36
|
+
The default browser fingerprint is Chrome. Choose another `curl_cffi` fingerprint or configure proxies by supplying your own `curl_cffi.requests.Session`:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from curl_cffi import requests
|
|
40
|
+
|
|
41
|
+
session = requests.Session(impersonate="safari")
|
|
42
|
+
client = Sub2API("https://sub2api.example.com", session=session)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Log in with email and password
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from sub2api import Sub2API
|
|
49
|
+
|
|
50
|
+
with Sub2API("https://sub2api.example.com") as client:
|
|
51
|
+
user = client.login("person@example.com", "password")
|
|
52
|
+
print(user.username)
|
|
53
|
+
print(client.is_authenticated)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
An instance with CAPTCHA enabled requires the corresponding proof:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
client.login(
|
|
60
|
+
"person@example.com",
|
|
61
|
+
"password",
|
|
62
|
+
turnstile_token="captcha-proof",
|
|
63
|
+
)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
For a TOTP-enabled account, `login()` raises `TwoFactorRequired` and retains the temporary challenge in memory:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from sub2api import Sub2API, TwoFactorRequired
|
|
70
|
+
|
|
71
|
+
client = Sub2API("https://sub2api.example.com")
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
client.login("person@example.com", "password")
|
|
75
|
+
except TwoFactorRequired:
|
|
76
|
+
client.complete_2fa("123456")
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Common operations
|
|
80
|
+
|
|
81
|
+
Resources are callable for their common list operation and also expose explicit methods.
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
balance = client.balance()
|
|
85
|
+
quotas = client.account.platform_quotas()
|
|
86
|
+
|
|
87
|
+
groups = client.groups()
|
|
88
|
+
group_rates = client.groups.rates()
|
|
89
|
+
|
|
90
|
+
first_page = client.keys(page_size=50, status="active")
|
|
91
|
+
for api_key in first_page:
|
|
92
|
+
print(api_key.id, api_key.name, api_key.group.name)
|
|
93
|
+
|
|
94
|
+
all_keys = client.keys.all()
|
|
95
|
+
resolved = client.keys.with_group_multipliers()
|
|
96
|
+
for item in resolved:
|
|
97
|
+
print(
|
|
98
|
+
item.api_key.key,
|
|
99
|
+
item.group_id,
|
|
100
|
+
item.base_multiplier,
|
|
101
|
+
item.custom_multiplier,
|
|
102
|
+
item.effective_multiplier,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
multiplier_by_key = client.keys.multiplier_map(key_by="key")
|
|
106
|
+
multiplier_by_id = client.keys.multiplier_map(key_by="id")
|
|
107
|
+
|
|
108
|
+
created = client.keys.create("automation", group_id=groups[0].id)
|
|
109
|
+
client.keys.update(created.id, name="nightly automation")
|
|
110
|
+
client.keys.set_status(created.id, active=False)
|
|
111
|
+
client.keys.delete(created.id)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`all()` follows pagination until every key has been fetched. `with_group_multipliers()` joins each key to its group and reports the base, user-specific, and effective rate; the user-specific rate from `/groups/rates` takes precedence. `multiplier_map()` returns the effective rate keyed by the API key value, key ID, or name. Name collisions raise an error instead of silently overwriting an entry.
|
|
115
|
+
|
|
116
|
+
API key values are available through `api_key.key`, but object representations redact fields that commonly contain credentials.
|
|
117
|
+
|
|
118
|
+
## Usage history
|
|
119
|
+
|
|
120
|
+
`history` and `usage` refer to the same resource.
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from datetime import date, timedelta
|
|
124
|
+
|
|
125
|
+
end = date.today()
|
|
126
|
+
start = end - timedelta(days=7)
|
|
127
|
+
|
|
128
|
+
page = client.history(
|
|
129
|
+
start_date=start,
|
|
130
|
+
end_date=end,
|
|
131
|
+
page_size=100,
|
|
132
|
+
sort_by="created_at",
|
|
133
|
+
sort_order="desc",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
for record in page:
|
|
137
|
+
print(record.created_at, record.model, record.actual_cost)
|
|
138
|
+
|
|
139
|
+
for record in client.history.iter(page_size=100):
|
|
140
|
+
process(record)
|
|
141
|
+
|
|
142
|
+
stats = client.usage.stats(start_date=start, end_date=end)
|
|
143
|
+
dashboard = client.usage.dashboard()
|
|
144
|
+
trend = client.usage.trend(start_date=start, end_date=end, granularity="day")
|
|
145
|
+
models = client.usage.models(start_date=start, end_date=end)
|
|
146
|
+
snapshot = client.usage.snapshot(start_date=start, end_date=end)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Other shared resources
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
active_subscriptions = client.subscriptions(active=True)
|
|
153
|
+
announcements = client.announcements()
|
|
154
|
+
client.announcements.mark_read(announcements[0].id)
|
|
155
|
+
|
|
156
|
+
result = client.redeem("REDEMPTION-CODE")
|
|
157
|
+
redemption_history = client.redeem.history()
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Fork-specific endpoints
|
|
161
|
+
|
|
162
|
+
`request()` provides the same authentication, envelope handling, timezone parameter, refresh behavior, and error mapping for relative endpoints that are not part of the stable resource API.
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
result = client.request("GET", "some-fork-specific-endpoint")
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Absolute URLs and parent-path traversal are rejected so a session token cannot be redirected outside the configured API root.
|
|
169
|
+
|
|
170
|
+
## Errors
|
|
171
|
+
|
|
172
|
+
HTTP and Sub2API envelope failures use typed exceptions:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
from sub2api import AuthenticationError, RateLimitError, Sub2APIError
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
client.keys.create("automation")
|
|
179
|
+
except RateLimitError as error:
|
|
180
|
+
print(error.retry_after)
|
|
181
|
+
except AuthenticationError:
|
|
182
|
+
client.login("person@example.com", "password")
|
|
183
|
+
except Sub2APIError as error:
|
|
184
|
+
print(error)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Remote plaintext HTTP is rejected by default because it exposes login credentials and tokens. Localhost HTTP is allowed for development; other HTTP instances require `allow_insecure=True`.
|
|
188
|
+
|
|
189
|
+
## Development
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
python -m pip install -e '.[dev]'
|
|
193
|
+
pytest
|
|
194
|
+
ruff check .
|
|
195
|
+
mypy
|
|
196
|
+
python -m build
|
|
197
|
+
twine check dist/*
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
HAR captures are ignored by Git because they can contain live session credentials.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.26"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sub2api"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python client for the user-facing API of Sub2API instances"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { file = "LICENSE" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Eight Labs" }
|
|
14
|
+
]
|
|
15
|
+
keywords = ["sub2api", "api", "sdk", "ai-gateway"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Typing :: Typed"
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"curl-cffi>=0.10,<1"
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
dev = [
|
|
33
|
+
"build>=1.2",
|
|
34
|
+
"httpx>=0.27,<1",
|
|
35
|
+
"mypy>=1.11",
|
|
36
|
+
"pytest>=8",
|
|
37
|
+
"pytest-cov>=5",
|
|
38
|
+
"ruff>=0.7",
|
|
39
|
+
"twine>=5"
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.wheel]
|
|
43
|
+
packages = ["src/sub2api"]
|
|
44
|
+
|
|
45
|
+
[tool.pytest.ini_options]
|
|
46
|
+
addopts = "-ra"
|
|
47
|
+
testpaths = ["tests"]
|
|
48
|
+
|
|
49
|
+
[tool.ruff]
|
|
50
|
+
line-length = 100
|
|
51
|
+
target-version = "py310"
|
|
52
|
+
|
|
53
|
+
[tool.ruff.lint]
|
|
54
|
+
select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
|
|
55
|
+
|
|
56
|
+
[tool.mypy]
|
|
57
|
+
python_version = "3.10"
|
|
58
|
+
strict = true
|
|
59
|
+
packages = ["sub2api"]
|
|
60
|
+
mypy_path = "src"
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from ._client import Sub2API
|
|
2
|
+
from ._exceptions import (
|
|
3
|
+
APIError,
|
|
4
|
+
AuthenticationError,
|
|
5
|
+
ConfigurationError,
|
|
6
|
+
ConflictError,
|
|
7
|
+
NotFoundError,
|
|
8
|
+
PermissionDeniedError,
|
|
9
|
+
ProtocolError,
|
|
10
|
+
RateLimitError,
|
|
11
|
+
Sub2APIError,
|
|
12
|
+
TransportError,
|
|
13
|
+
TwoFactorRequired,
|
|
14
|
+
ValidationError,
|
|
15
|
+
)
|
|
16
|
+
from ._models import (
|
|
17
|
+
Announcement,
|
|
18
|
+
APIKey,
|
|
19
|
+
Balance,
|
|
20
|
+
Group,
|
|
21
|
+
KeyGroupMultiplier,
|
|
22
|
+
Page,
|
|
23
|
+
PlatformQuota,
|
|
24
|
+
Redemption,
|
|
25
|
+
Resource,
|
|
26
|
+
SessionTokens,
|
|
27
|
+
Subscription,
|
|
28
|
+
UsageRecord,
|
|
29
|
+
User,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"APIError",
|
|
34
|
+
"APIKey",
|
|
35
|
+
"Announcement",
|
|
36
|
+
"AuthenticationError",
|
|
37
|
+
"Balance",
|
|
38
|
+
"ConfigurationError",
|
|
39
|
+
"ConflictError",
|
|
40
|
+
"Group",
|
|
41
|
+
"KeyGroupMultiplier",
|
|
42
|
+
"NotFoundError",
|
|
43
|
+
"Page",
|
|
44
|
+
"PermissionDeniedError",
|
|
45
|
+
"PlatformQuota",
|
|
46
|
+
"ProtocolError",
|
|
47
|
+
"RateLimitError",
|
|
48
|
+
"Redemption",
|
|
49
|
+
"Resource",
|
|
50
|
+
"SessionTokens",
|
|
51
|
+
"Sub2API",
|
|
52
|
+
"Sub2APIError",
|
|
53
|
+
"Subscription",
|
|
54
|
+
"TransportError",
|
|
55
|
+
"TwoFactorRequired",
|
|
56
|
+
"UsageRecord",
|
|
57
|
+
"User",
|
|
58
|
+
"ValidationError",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
__version__ = "0.1.0"
|