nexalayer 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.
- nexalayer-0.1.0/LICENSE +21 -0
- nexalayer-0.1.0/PKG-INFO +211 -0
- nexalayer-0.1.0/README.md +188 -0
- nexalayer-0.1.0/pyproject.toml +45 -0
- nexalayer-0.1.0/python/nexalayer/__init__.py +28 -0
- nexalayer-0.1.0/python/nexalayer/client.py +240 -0
- nexalayer-0.1.0/python/nexalayer/errors.py +30 -0
- nexalayer-0.1.0/python/nexalayer/session.py +46 -0
- nexalayer-0.1.0/python/nexalayer/types.py +59 -0
- nexalayer-0.1.0/python/nexalayer.egg-info/PKG-INFO +211 -0
- nexalayer-0.1.0/python/nexalayer.egg-info/SOURCES.txt +13 -0
- nexalayer-0.1.0/python/nexalayer.egg-info/dependency_links.txt +1 -0
- nexalayer-0.1.0/python/nexalayer.egg-info/requires.txt +5 -0
- nexalayer-0.1.0/python/nexalayer.egg-info/top_level.txt +1 -0
- nexalayer-0.1.0/setup.cfg +4 -0
nexalayer-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 NexaLayer
|
|
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.
|
nexalayer-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nexalayer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: NexaLayer SDK — The Network Execution Layer for AI Agents
|
|
5
|
+
Author: NexaLayer
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Documentation, https://docs.nexalayer.net
|
|
8
|
+
Project-URL: Repository, https://github.com/NexaLayer/nexalayer-sdk
|
|
9
|
+
Project-URL: Issues, https://github.com/NexaLayer/nexalayer-sdk/issues
|
|
10
|
+
Keywords: proxy,agent,session,nexalayer
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: requests>=2.28
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
21
|
+
Requires-Dist: pytest-cov>=4; extra == "dev"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# NexaLayer SDK
|
|
25
|
+
|
|
26
|
+
Managed network sessions for Playwright, Browser Use, AI agents, and automation scripts.
|
|
27
|
+
|
|
28
|
+
**Keep using proxies. Stop managing them.**
|
|
29
|
+
中文:代理继续使用,代理管理交给 NexaLayer。
|
|
30
|
+
|
|
31
|
+
This repository contains the Python and Node/TypeScript SDK source. Release automation publishes signed release artifacts to PyPI and npm after registry ownership and secrets are configured.
|
|
32
|
+
|
|
33
|
+
## Status
|
|
34
|
+
|
|
35
|
+
| Runtime | Source | Package registry status |
|
|
36
|
+
| --- | --- | --- |
|
|
37
|
+
| Python | `python/` | Package name `nexalayer`; release workflow configured |
|
|
38
|
+
| Node / TypeScript | `node/` | Package name `@nexalayer/sdk`; release workflow configured |
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
Python from repository root:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
python3 -m venv .venv
|
|
46
|
+
. .venv/bin/activate
|
|
47
|
+
pip install -e ".[dev]"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Node from repository root:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
cd node
|
|
54
|
+
npm install
|
|
55
|
+
npm run build
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Authentication
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
export NEXALAYER_BASE_URL=https://api.nexalayer.net/v1
|
|
62
|
+
export NEXALAYER_API_KEY=agk_your_key
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The SDK uses `X-API-Key` for Agent requests.
|
|
66
|
+
|
|
67
|
+
## 60-second Python example
|
|
68
|
+
|
|
69
|
+
This example auto-selects a dynamic product, creates a session, reports telemetry, reads health, and terminates the session in cleanup.
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
import os
|
|
73
|
+
import time
|
|
74
|
+
import uuid
|
|
75
|
+
|
|
76
|
+
import requests
|
|
77
|
+
from nexalayer import NexaLayerClient
|
|
78
|
+
|
|
79
|
+
api_key = os.environ["NEXALAYER_API_KEY"]
|
|
80
|
+
client = NexaLayerClient(api_key=api_key)
|
|
81
|
+
session_id = None
|
|
82
|
+
|
|
83
|
+
def data(resp):
|
|
84
|
+
return resp.get("data", resp)
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
products = data(client.get_products(type="dynamic"))
|
|
88
|
+
product = next(p for p in products.get("items", []) if p.get("product_no"))
|
|
89
|
+
|
|
90
|
+
session = client.create_session(
|
|
91
|
+
session_type="dynamic",
|
|
92
|
+
product_no=product["product_no"],
|
|
93
|
+
quantity=1,
|
|
94
|
+
protocol="socks5",
|
|
95
|
+
rotation_mode="on_demand",
|
|
96
|
+
idempotency_key=f"py-{uuid.uuid4()}",
|
|
97
|
+
)
|
|
98
|
+
session_id = session.session_id
|
|
99
|
+
|
|
100
|
+
for _ in range(60):
|
|
101
|
+
current = data(client.get_session(session_id))
|
|
102
|
+
if current.get("status") == "active":
|
|
103
|
+
proxy_url = current["proxy"]["full_url"]
|
|
104
|
+
break
|
|
105
|
+
time.sleep(2)
|
|
106
|
+
else:
|
|
107
|
+
raise TimeoutError("Session did not become active")
|
|
108
|
+
|
|
109
|
+
res = requests.get(
|
|
110
|
+
"https://httpbin.org/ip",
|
|
111
|
+
proxies={"http": proxy_url, "https": proxy_url},
|
|
112
|
+
timeout=30,
|
|
113
|
+
)
|
|
114
|
+
client.report_event(
|
|
115
|
+
session_id,
|
|
116
|
+
event_type="success" if res.ok else "http_error",
|
|
117
|
+
status_code=res.status_code,
|
|
118
|
+
target_host="httpbin.org",
|
|
119
|
+
)
|
|
120
|
+
print(client.get_session_health(session_id))
|
|
121
|
+
finally:
|
|
122
|
+
if session_id:
|
|
123
|
+
client.terminate_session(session_id)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Dynamic Session
|
|
127
|
+
|
|
128
|
+
Use a dynamic session when your automation can rotate after errors such as timeout, 403, 429, or explicit blocks.
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
session = client.create_session(
|
|
132
|
+
session_type="dynamic",
|
|
133
|
+
product_no="selected_from_products_api",
|
|
134
|
+
quantity=1,
|
|
135
|
+
protocol="socks5",
|
|
136
|
+
rotation_mode="on_demand",
|
|
137
|
+
)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Static Session
|
|
141
|
+
|
|
142
|
+
Use a static session when you need fixed network identity for a longer-lived task.
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
session = client.create_session(
|
|
146
|
+
session_type="static",
|
|
147
|
+
product_no="selected_from_products_api",
|
|
148
|
+
quantity=1,
|
|
149
|
+
duration=30,
|
|
150
|
+
protocol="socks5",
|
|
151
|
+
)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Telemetry
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
client.report_event(
|
|
158
|
+
session_id,
|
|
159
|
+
event_type="timeout",
|
|
160
|
+
status_code=0,
|
|
161
|
+
target_host="example.com",
|
|
162
|
+
)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Health
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
health = client.get_session_health(session_id)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`timeout`, `captcha`, `block`, and repeated HTTP errors are not neutral noise; report them so NexaLayer can score the session and recommend rotate or pause.
|
|
172
|
+
|
|
173
|
+
## Cleanup
|
|
174
|
+
|
|
175
|
+
Always terminate sessions you no longer need:
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
try:
|
|
179
|
+
...
|
|
180
|
+
finally:
|
|
181
|
+
if session_id:
|
|
182
|
+
client.terminate_session(session_id)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Playwright
|
|
186
|
+
|
|
187
|
+
Use the public Playwright examples:
|
|
188
|
+
|
|
189
|
+
- Chinese: https://github.com/NexaLayer/nexalayer-examples/tree/main/playwright/basic-session
|
|
190
|
+
- English: https://github.com/NexaLayer/nexalayer-examples/tree/main/playwright/basic-session
|
|
191
|
+
|
|
192
|
+
## 中文文档
|
|
193
|
+
|
|
194
|
+
- 中文快速开始: https://docs.nexalayer.net/zh/quick-start
|
|
195
|
+
- Playwright 中文 Demo: https://docs.nexalayer.net/zh/examples
|
|
196
|
+
- API 文档: https://docs.nexalayer.net/api-reference/openapi
|
|
197
|
+
|
|
198
|
+
## Error handling
|
|
199
|
+
|
|
200
|
+
- Check `backend_ready=true` before creating paid sessions after registration.
|
|
201
|
+
- Use `Idempotency-Key` for create, rotate, renew, and recharge operations.
|
|
202
|
+
- Do not log `proxy.full_url` in production without redaction because it can contain proxy credentials.
|
|
203
|
+
- Respect target website terms and applicable law.
|
|
204
|
+
|
|
205
|
+
## Contributing
|
|
206
|
+
|
|
207
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
208
|
+
|
|
209
|
+
## Security
|
|
210
|
+
|
|
211
|
+
Do not open public issues with API keys, cookies, account passwords, proxy credentials, or target-site secrets. See [SECURITY.md](SECURITY.md).
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# NexaLayer SDK
|
|
2
|
+
|
|
3
|
+
Managed network sessions for Playwright, Browser Use, AI agents, and automation scripts.
|
|
4
|
+
|
|
5
|
+
**Keep using proxies. Stop managing them.**
|
|
6
|
+
中文:代理继续使用,代理管理交给 NexaLayer。
|
|
7
|
+
|
|
8
|
+
This repository contains the Python and Node/TypeScript SDK source. Release automation publishes signed release artifacts to PyPI and npm after registry ownership and secrets are configured.
|
|
9
|
+
|
|
10
|
+
## Status
|
|
11
|
+
|
|
12
|
+
| Runtime | Source | Package registry status |
|
|
13
|
+
| --- | --- | --- |
|
|
14
|
+
| Python | `python/` | Package name `nexalayer`; release workflow configured |
|
|
15
|
+
| Node / TypeScript | `node/` | Package name `@nexalayer/sdk`; release workflow configured |
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
Python from repository root:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
python3 -m venv .venv
|
|
23
|
+
. .venv/bin/activate
|
|
24
|
+
pip install -e ".[dev]"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Node from repository root:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
cd node
|
|
31
|
+
npm install
|
|
32
|
+
npm run build
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Authentication
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
export NEXALAYER_BASE_URL=https://api.nexalayer.net/v1
|
|
39
|
+
export NEXALAYER_API_KEY=agk_your_key
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The SDK uses `X-API-Key` for Agent requests.
|
|
43
|
+
|
|
44
|
+
## 60-second Python example
|
|
45
|
+
|
|
46
|
+
This example auto-selects a dynamic product, creates a session, reports telemetry, reads health, and terminates the session in cleanup.
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
import os
|
|
50
|
+
import time
|
|
51
|
+
import uuid
|
|
52
|
+
|
|
53
|
+
import requests
|
|
54
|
+
from nexalayer import NexaLayerClient
|
|
55
|
+
|
|
56
|
+
api_key = os.environ["NEXALAYER_API_KEY"]
|
|
57
|
+
client = NexaLayerClient(api_key=api_key)
|
|
58
|
+
session_id = None
|
|
59
|
+
|
|
60
|
+
def data(resp):
|
|
61
|
+
return resp.get("data", resp)
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
products = data(client.get_products(type="dynamic"))
|
|
65
|
+
product = next(p for p in products.get("items", []) if p.get("product_no"))
|
|
66
|
+
|
|
67
|
+
session = client.create_session(
|
|
68
|
+
session_type="dynamic",
|
|
69
|
+
product_no=product["product_no"],
|
|
70
|
+
quantity=1,
|
|
71
|
+
protocol="socks5",
|
|
72
|
+
rotation_mode="on_demand",
|
|
73
|
+
idempotency_key=f"py-{uuid.uuid4()}",
|
|
74
|
+
)
|
|
75
|
+
session_id = session.session_id
|
|
76
|
+
|
|
77
|
+
for _ in range(60):
|
|
78
|
+
current = data(client.get_session(session_id))
|
|
79
|
+
if current.get("status") == "active":
|
|
80
|
+
proxy_url = current["proxy"]["full_url"]
|
|
81
|
+
break
|
|
82
|
+
time.sleep(2)
|
|
83
|
+
else:
|
|
84
|
+
raise TimeoutError("Session did not become active")
|
|
85
|
+
|
|
86
|
+
res = requests.get(
|
|
87
|
+
"https://httpbin.org/ip",
|
|
88
|
+
proxies={"http": proxy_url, "https": proxy_url},
|
|
89
|
+
timeout=30,
|
|
90
|
+
)
|
|
91
|
+
client.report_event(
|
|
92
|
+
session_id,
|
|
93
|
+
event_type="success" if res.ok else "http_error",
|
|
94
|
+
status_code=res.status_code,
|
|
95
|
+
target_host="httpbin.org",
|
|
96
|
+
)
|
|
97
|
+
print(client.get_session_health(session_id))
|
|
98
|
+
finally:
|
|
99
|
+
if session_id:
|
|
100
|
+
client.terminate_session(session_id)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Dynamic Session
|
|
104
|
+
|
|
105
|
+
Use a dynamic session when your automation can rotate after errors such as timeout, 403, 429, or explicit blocks.
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
session = client.create_session(
|
|
109
|
+
session_type="dynamic",
|
|
110
|
+
product_no="selected_from_products_api",
|
|
111
|
+
quantity=1,
|
|
112
|
+
protocol="socks5",
|
|
113
|
+
rotation_mode="on_demand",
|
|
114
|
+
)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Static Session
|
|
118
|
+
|
|
119
|
+
Use a static session when you need fixed network identity for a longer-lived task.
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
session = client.create_session(
|
|
123
|
+
session_type="static",
|
|
124
|
+
product_no="selected_from_products_api",
|
|
125
|
+
quantity=1,
|
|
126
|
+
duration=30,
|
|
127
|
+
protocol="socks5",
|
|
128
|
+
)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Telemetry
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
client.report_event(
|
|
135
|
+
session_id,
|
|
136
|
+
event_type="timeout",
|
|
137
|
+
status_code=0,
|
|
138
|
+
target_host="example.com",
|
|
139
|
+
)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Health
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
health = client.get_session_health(session_id)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`timeout`, `captcha`, `block`, and repeated HTTP errors are not neutral noise; report them so NexaLayer can score the session and recommend rotate or pause.
|
|
149
|
+
|
|
150
|
+
## Cleanup
|
|
151
|
+
|
|
152
|
+
Always terminate sessions you no longer need:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
try:
|
|
156
|
+
...
|
|
157
|
+
finally:
|
|
158
|
+
if session_id:
|
|
159
|
+
client.terminate_session(session_id)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Playwright
|
|
163
|
+
|
|
164
|
+
Use the public Playwright examples:
|
|
165
|
+
|
|
166
|
+
- Chinese: https://github.com/NexaLayer/nexalayer-examples/tree/main/playwright/basic-session
|
|
167
|
+
- English: https://github.com/NexaLayer/nexalayer-examples/tree/main/playwright/basic-session
|
|
168
|
+
|
|
169
|
+
## 中文文档
|
|
170
|
+
|
|
171
|
+
- 中文快速开始: https://docs.nexalayer.net/zh/quick-start
|
|
172
|
+
- Playwright 中文 Demo: https://docs.nexalayer.net/zh/examples
|
|
173
|
+
- API 文档: https://docs.nexalayer.net/api-reference/openapi
|
|
174
|
+
|
|
175
|
+
## Error handling
|
|
176
|
+
|
|
177
|
+
- Check `backend_ready=true` before creating paid sessions after registration.
|
|
178
|
+
- Use `Idempotency-Key` for create, rotate, renew, and recharge operations.
|
|
179
|
+
- Do not log `proxy.full_url` in production without redaction because it can contain proxy credentials.
|
|
180
|
+
- Respect target website terms and applicable law.
|
|
181
|
+
|
|
182
|
+
## Contributing
|
|
183
|
+
|
|
184
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
185
|
+
|
|
186
|
+
## Security
|
|
187
|
+
|
|
188
|
+
Do not open public issues with API keys, cookies, account passwords, proxy credentials, or target-site secrets. See [SECURITY.md](SECURITY.md).
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "nexalayer"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "NexaLayer SDK — The Network Execution Layer for AI Agents"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [{ name = "NexaLayer" }]
|
|
13
|
+
keywords = ["proxy", "agent", "session", "nexalayer"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.9",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"requests>=2.28",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
dev = [
|
|
26
|
+
"pytest>=7",
|
|
27
|
+
"pytest-cov>=4",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Documentation = "https://docs.nexalayer.net"
|
|
32
|
+
Repository = "https://github.com/NexaLayer/nexalayer-sdk"
|
|
33
|
+
Issues = "https://github.com/NexaLayer/nexalayer-sdk/issues"
|
|
34
|
+
|
|
35
|
+
[tool.setuptools.packages.find]
|
|
36
|
+
where = ["python"]
|
|
37
|
+
exclude = ["tests*"]
|
|
38
|
+
|
|
39
|
+
[tool.pytest.ini_options]
|
|
40
|
+
testpaths = ["python/tests"]
|
|
41
|
+
pythonpath = ["python"]
|
|
42
|
+
|
|
43
|
+
[tool.black]
|
|
44
|
+
line-length = 88
|
|
45
|
+
target-version = ["py39", "py310", "py311"]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""NexaLayer SDK — The Network Execution Layer for AI Agents."""
|
|
2
|
+
|
|
3
|
+
from nexalayer.client import NexaLayerClient
|
|
4
|
+
from nexalayer.session import Session
|
|
5
|
+
from nexalayer.errors import (
|
|
6
|
+
NexaLayerError,
|
|
7
|
+
AuthError,
|
|
8
|
+
APIError,
|
|
9
|
+
SessionError,
|
|
10
|
+
)
|
|
11
|
+
from nexalayer.types import (
|
|
12
|
+
SessionConfig,
|
|
13
|
+
SessionCreateResponse,
|
|
14
|
+
TokenResponse,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__version__ = "0.1.0"
|
|
18
|
+
__all__ = [
|
|
19
|
+
"NexaLayerClient",
|
|
20
|
+
"Session",
|
|
21
|
+
"NexaLayerError",
|
|
22
|
+
"AuthError",
|
|
23
|
+
"APIError",
|
|
24
|
+
"SessionError",
|
|
25
|
+
"SessionConfig",
|
|
26
|
+
"SessionCreateResponse",
|
|
27
|
+
"TokenResponse",
|
|
28
|
+
]
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""NexaLayer API client — auth, account, billing, products, sessions, stats."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional, Union
|
|
4
|
+
from urllib.parse import urlencode
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from nexalayer.errors import APIError, AuthError
|
|
9
|
+
from nexalayer.session import Session
|
|
10
|
+
from nexalayer.types import SessionConfig, SessionCreateResponse, TokenResponse
|
|
11
|
+
|
|
12
|
+
DEFAULT_BASE_URL = "https://api.nexalayer.net/v1"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class NexaLayerClient:
|
|
16
|
+
"""HTTP client for NexaLayer API with auth header injection."""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
api_key: Optional[str] = None,
|
|
21
|
+
bearer_token: Optional[str] = None,
|
|
22
|
+
api_secret: Optional[str] = None,
|
|
23
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
24
|
+
):
|
|
25
|
+
self.base_url = base_url.rstrip("/")
|
|
26
|
+
self._api_key = api_key
|
|
27
|
+
self._bearer_token = bearer_token
|
|
28
|
+
self._api_secret = api_secret
|
|
29
|
+
self._session = requests.Session()
|
|
30
|
+
if bearer_token:
|
|
31
|
+
self._session.headers["Authorization"] = f"Bearer {bearer_token}"
|
|
32
|
+
elif api_key:
|
|
33
|
+
self._session.headers["X-API-Key"] = api_key
|
|
34
|
+
|
|
35
|
+
def _headers(self) -> dict:
|
|
36
|
+
h = {"Content-Type": "application/json", "Accept": "application/json"}
|
|
37
|
+
if self._bearer_token:
|
|
38
|
+
h["Authorization"] = f"Bearer {self._bearer_token}"
|
|
39
|
+
elif self._api_key:
|
|
40
|
+
h["X-API-Key"] = self._api_key
|
|
41
|
+
return h
|
|
42
|
+
|
|
43
|
+
def _request(
|
|
44
|
+
self,
|
|
45
|
+
method: str,
|
|
46
|
+
path: str,
|
|
47
|
+
json: Optional[dict] = None,
|
|
48
|
+
headers: Optional[dict] = None,
|
|
49
|
+
**kwargs: Any,
|
|
50
|
+
) -> dict:
|
|
51
|
+
url = f"{self.base_url}{path}"
|
|
52
|
+
request_headers = self._headers()
|
|
53
|
+
if headers:
|
|
54
|
+
request_headers.update(headers)
|
|
55
|
+
resp = self._session.request(
|
|
56
|
+
method,
|
|
57
|
+
url,
|
|
58
|
+
json=json,
|
|
59
|
+
headers=request_headers,
|
|
60
|
+
timeout=kwargs.get("timeout", 30),
|
|
61
|
+
)
|
|
62
|
+
if resp.status_code >= 400:
|
|
63
|
+
raise APIError(
|
|
64
|
+
f"API error: {resp.status_code}",
|
|
65
|
+
status_code=resp.status_code,
|
|
66
|
+
body=resp.text,
|
|
67
|
+
)
|
|
68
|
+
return resp.json() if resp.content else {}
|
|
69
|
+
|
|
70
|
+
def get_token(self, api_key: Optional[str] = None, api_secret: Optional[str] = None) -> TokenResponse:
|
|
71
|
+
"""POST /auth/token — obtain Bearer token from API Key/Secret."""
|
|
72
|
+
# TODO: call real POST /auth/token when backend is available
|
|
73
|
+
key = api_key or self._api_key
|
|
74
|
+
secret = api_secret or self._api_secret
|
|
75
|
+
if not key or not secret:
|
|
76
|
+
raise AuthError("api_key and api_secret required for get_token")
|
|
77
|
+
payload = {"api_key": key, "api_secret": secret}
|
|
78
|
+
data = self._request("POST", "/auth/token", json=payload)
|
|
79
|
+
# Placeholder; replace with real response parsing
|
|
80
|
+
token = data.get("data", {}).get("access_token") or "mock-token"
|
|
81
|
+
return TokenResponse(access_token=token)
|
|
82
|
+
|
|
83
|
+
def register(
|
|
84
|
+
self,
|
|
85
|
+
name: str,
|
|
86
|
+
contact_email: str,
|
|
87
|
+
referral_code: Optional[str] = None,
|
|
88
|
+
**kwargs: Any,
|
|
89
|
+
) -> dict:
|
|
90
|
+
"""POST /account/register."""
|
|
91
|
+
# TODO: implement real call
|
|
92
|
+
payload: dict = {"name": name, "contact_email": contact_email, **kwargs}
|
|
93
|
+
if referral_code is not None:
|
|
94
|
+
payload["referral_code"] = referral_code
|
|
95
|
+
return self._request("POST", "/account/register", json=payload)
|
|
96
|
+
|
|
97
|
+
def request_api_key(self, email: str, referral_code: Optional[str] = None) -> dict:
|
|
98
|
+
"""POST /account/request-api-key; first-time emails receive trial credit."""
|
|
99
|
+
payload = {"email": email}
|
|
100
|
+
if referral_code:
|
|
101
|
+
payload["referral_code"] = referral_code
|
|
102
|
+
return self._request("POST", "/account/request-api-key", json=payload)
|
|
103
|
+
|
|
104
|
+
def get_balance(self) -> dict:
|
|
105
|
+
"""GET /billing/balance."""
|
|
106
|
+
# TODO: implement real call
|
|
107
|
+
return self._request("GET", "/billing/balance")
|
|
108
|
+
|
|
109
|
+
def recharge(
|
|
110
|
+
self,
|
|
111
|
+
amount: float,
|
|
112
|
+
currency: str = "USD",
|
|
113
|
+
idempotency_key: Optional[str] = None,
|
|
114
|
+
**kwargs: Any,
|
|
115
|
+
) -> dict:
|
|
116
|
+
"""POST /billing/recharge."""
|
|
117
|
+
# TODO: implement real call
|
|
118
|
+
payload = {"amount": amount, "currency": currency, **kwargs}
|
|
119
|
+
headers = {"Idempotency-Key": idempotency_key} if idempotency_key else None
|
|
120
|
+
return self._request("POST", "/billing/recharge", json=payload, headers=headers)
|
|
121
|
+
|
|
122
|
+
def get_budget(self) -> dict:
|
|
123
|
+
"""GET /account/budget."""
|
|
124
|
+
return self._request("GET", "/account/budget")
|
|
125
|
+
|
|
126
|
+
def set_budget(
|
|
127
|
+
self,
|
|
128
|
+
*,
|
|
129
|
+
idempotency_key: str,
|
|
130
|
+
key_limit_usd: Optional[float] = None,
|
|
131
|
+
daily_limit_usd: Optional[float] = None,
|
|
132
|
+
pause_on_exceed: bool = True,
|
|
133
|
+
alert_thresholds: Optional[list] = None,
|
|
134
|
+
paused: Optional[bool] = None,
|
|
135
|
+
) -> dict:
|
|
136
|
+
"""PUT /account/budget."""
|
|
137
|
+
if not idempotency_key:
|
|
138
|
+
raise ValueError("idempotency_key is required")
|
|
139
|
+
payload = {
|
|
140
|
+
"key_limit_usd": key_limit_usd,
|
|
141
|
+
"daily_limit_usd": daily_limit_usd,
|
|
142
|
+
"pause_on_exceed": pause_on_exceed,
|
|
143
|
+
"alert_thresholds": alert_thresholds or [50, 80, 100],
|
|
144
|
+
}
|
|
145
|
+
if paused is not None:
|
|
146
|
+
payload["paused"] = paused
|
|
147
|
+
return self._request(
|
|
148
|
+
"PUT",
|
|
149
|
+
"/account/budget",
|
|
150
|
+
json=payload,
|
|
151
|
+
headers={"Idempotency-Key": idempotency_key},
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def get_usage_alerts(self) -> dict:
|
|
155
|
+
"""GET /account/usage-alerts."""
|
|
156
|
+
return self._request("GET", "/account/usage-alerts")
|
|
157
|
+
|
|
158
|
+
def get_products(self, type: str = "all", country_code: Optional[str] = None) -> dict:
|
|
159
|
+
"""GET /products."""
|
|
160
|
+
query = {"type": type}
|
|
161
|
+
if country_code:
|
|
162
|
+
query["country_code"] = country_code
|
|
163
|
+
return self._request("GET", f"/products?{urlencode(query)}")
|
|
164
|
+
|
|
165
|
+
def recommend_products(self, criteria: dict) -> dict:
|
|
166
|
+
"""POST /products/recommend."""
|
|
167
|
+
# TODO: implement real call
|
|
168
|
+
return self._request("POST", "/products/recommend", json=criteria)
|
|
169
|
+
|
|
170
|
+
def create_session(
|
|
171
|
+
self,
|
|
172
|
+
type: Optional[str] = None,
|
|
173
|
+
session_type: Optional[str] = None,
|
|
174
|
+
product_no: Optional[str] = None,
|
|
175
|
+
config: Optional[Union[dict, SessionConfig]] = None,
|
|
176
|
+
idempotency_key: Optional[str] = None,
|
|
177
|
+
**kwargs: Any,
|
|
178
|
+
) -> Session:
|
|
179
|
+
"""POST /sessions — create a session and return a Session object."""
|
|
180
|
+
cfg = config.to_dict() if isinstance(config, SessionConfig) else (config or {})
|
|
181
|
+
resolved_type = session_type or type or cfg.pop("session_type", "dynamic")
|
|
182
|
+
resolved_product = product_no or cfg.pop("product_no", None)
|
|
183
|
+
if not resolved_product:
|
|
184
|
+
raise APIError("product_no is required")
|
|
185
|
+
payload = {
|
|
186
|
+
"session_type": resolved_type,
|
|
187
|
+
"product_no": resolved_product,
|
|
188
|
+
**cfg,
|
|
189
|
+
**kwargs,
|
|
190
|
+
}
|
|
191
|
+
headers = {"Idempotency-Key": idempotency_key} if idempotency_key else None
|
|
192
|
+
data = self._request("POST", "/sessions", json=payload, headers=headers)
|
|
193
|
+
inner = data.get("data", data)
|
|
194
|
+
session_id = inner.get("session_id")
|
|
195
|
+
if not session_id:
|
|
196
|
+
raise APIError(f"Session create response missing session_id: {data}")
|
|
197
|
+
status = inner.get("status") or "creating"
|
|
198
|
+
proxy_config = inner.get("proxy") or inner.get("proxy_config") or {}
|
|
199
|
+
resp = SessionCreateResponse(
|
|
200
|
+
session_id=session_id,
|
|
201
|
+
status=status,
|
|
202
|
+
proxy_config=proxy_config,
|
|
203
|
+
)
|
|
204
|
+
return Session(
|
|
205
|
+
client=self,
|
|
206
|
+
session_id=resp.session_id,
|
|
207
|
+
proxy_config=resp.proxy_config,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
def get_session(self, session_id: str) -> dict:
|
|
211
|
+
"""GET /sessions/{session_id}."""
|
|
212
|
+
# TODO: implement real call
|
|
213
|
+
return self._request("GET", f"/sessions/{session_id}")
|
|
214
|
+
|
|
215
|
+
def rotate_session(self, session_id: str, idempotency_key: Optional[str] = None) -> dict:
|
|
216
|
+
"""POST /sessions/{session_id}/rotate."""
|
|
217
|
+
headers = {"Idempotency-Key": idempotency_key} if idempotency_key else None
|
|
218
|
+
return self._request("POST", f"/sessions/{session_id}/rotate", headers=headers)
|
|
219
|
+
|
|
220
|
+
def terminate_session(self, session_id: str) -> dict:
|
|
221
|
+
"""DELETE /sessions/{session_id}."""
|
|
222
|
+
return self._request("DELETE", f"/sessions/{session_id}")
|
|
223
|
+
|
|
224
|
+
def report_event(self, session_id: str, **event: Any) -> dict:
|
|
225
|
+
"""POST /sessions/{session_id}/report-event."""
|
|
226
|
+
return self._request("POST", f"/sessions/{session_id}/report-event", json=event)
|
|
227
|
+
|
|
228
|
+
def get_session_health(self, session_id: str) -> dict:
|
|
229
|
+
"""GET /sessions/{session_id}/health."""
|
|
230
|
+
return self._request("GET", f"/sessions/{session_id}/health")
|
|
231
|
+
|
|
232
|
+
def get_session_usage(self, session_id: str) -> dict:
|
|
233
|
+
"""GET /sessions/{session_id}/usage."""
|
|
234
|
+
# TODO: implement real call
|
|
235
|
+
return self._request("GET", f"/sessions/{session_id}/usage")
|
|
236
|
+
|
|
237
|
+
def get_stats_overview(self) -> dict:
|
|
238
|
+
"""GET /stats/overview."""
|
|
239
|
+
# TODO: implement real call
|
|
240
|
+
return self._request("GET", "/stats/overview")
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""NexaLayer SDK error classes."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class NexaLayerError(Exception):
|
|
7
|
+
"""Base exception for NexaLayer SDK."""
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
message: str,
|
|
12
|
+
status_code: Optional[int] = None,
|
|
13
|
+
body: Optional[str] = None,
|
|
14
|
+
):
|
|
15
|
+
super().__init__(message)
|
|
16
|
+
self.message = message
|
|
17
|
+
self.status_code = status_code
|
|
18
|
+
self.body = body
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AuthError(NexaLayerError):
|
|
22
|
+
"""Authentication or authorization failed."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class APIError(NexaLayerError):
|
|
26
|
+
"""API returned an error (4xx/5xx)."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SessionError(NexaLayerError):
|
|
30
|
+
"""Session operation failed (create, rotate, get, etc.)."""
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Session abstraction for NexaLayer managed sessions."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
from nexalayer.errors import SessionError
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from nexalayer.client import NexaLayerClient
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Session:
|
|
14
|
+
"""Represents a NexaLayer proxy session; use get/post to send traffic via proxy."""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
client: "NexaLayerClient",
|
|
19
|
+
session_id: str,
|
|
20
|
+
proxy_config: Optional[dict] = None,
|
|
21
|
+
):
|
|
22
|
+
self.client = client
|
|
23
|
+
self.session_id = session_id
|
|
24
|
+
self.proxy_config = proxy_config or {}
|
|
25
|
+
self._session = requests.Session()
|
|
26
|
+
# Transport helpers are intentionally minimal. Production integrations
|
|
27
|
+
# should use proxy.full_url or host/port credentials from GET /sessions.
|
|
28
|
+
# self._session.proxies = {"http": ..., "https": ...}
|
|
29
|
+
|
|
30
|
+
def get(self, url: str, **kwargs: Any) -> requests.Response:
|
|
31
|
+
"""Perform GET with the local requests session."""
|
|
32
|
+
# TODO: use proxy_config to route via proxy when available
|
|
33
|
+
return self._session.get(url, timeout=kwargs.get("timeout", 30))
|
|
34
|
+
|
|
35
|
+
def post(self, url: str, data: Any = None, json: Any = None, **kwargs: Any) -> requests.Response:
|
|
36
|
+
"""Perform POST with the local requests session."""
|
|
37
|
+
# TODO: use proxy_config to route via proxy when available
|
|
38
|
+
return self._session.post(url, data=data, json=json, timeout=kwargs.get("timeout", 30))
|
|
39
|
+
|
|
40
|
+
def rotate(self) -> dict:
|
|
41
|
+
"""POST /sessions/{session_id}/rotate — rotate proxy for this session."""
|
|
42
|
+
return self.client.rotate_session(self.session_id)
|
|
43
|
+
|
|
44
|
+
def usage(self) -> dict:
|
|
45
|
+
"""GET /sessions/{session_id}/usage."""
|
|
46
|
+
return self.client.get_session_usage(self.session_id)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Lightweight request/response types for NexaLayer API."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SessionConfig:
|
|
7
|
+
"""Configuration for creating a session (dynamic or static)."""
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
product_no: Optional[str] = None,
|
|
12
|
+
traffic_gb: Optional[float] = None,
|
|
13
|
+
protocol: Optional[str] = None,
|
|
14
|
+
country: Optional[str] = None,
|
|
15
|
+
**kwargs: Any,
|
|
16
|
+
):
|
|
17
|
+
self.product_no = product_no
|
|
18
|
+
self.traffic_gb = traffic_gb
|
|
19
|
+
self.protocol = protocol
|
|
20
|
+
self.country = country
|
|
21
|
+
self.extra = kwargs
|
|
22
|
+
|
|
23
|
+
def to_dict(self) -> dict:
|
|
24
|
+
out = {}
|
|
25
|
+
if self.product_no is not None:
|
|
26
|
+
out["product_no"] = self.product_no
|
|
27
|
+
if self.traffic_gb is not None:
|
|
28
|
+
out["traffic_gb"] = self.traffic_gb
|
|
29
|
+
if self.protocol is not None:
|
|
30
|
+
out["protocol"] = self.protocol
|
|
31
|
+
if self.country is not None:
|
|
32
|
+
out["country"] = self.country
|
|
33
|
+
out.update(self.extra)
|
|
34
|
+
return out
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TokenResponse:
|
|
38
|
+
"""Response from POST /auth/token."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, access_token: str, token_type: str = "Bearer", **kwargs: Any):
|
|
41
|
+
self.access_token = access_token
|
|
42
|
+
self.token_type = token_type
|
|
43
|
+
self.extra = kwargs
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class SessionCreateResponse:
|
|
47
|
+
"""Response from POST /sessions."""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
session_id: str,
|
|
52
|
+
status: str,
|
|
53
|
+
proxy_config: Optional[dict] = None,
|
|
54
|
+
**kwargs: Any,
|
|
55
|
+
):
|
|
56
|
+
self.session_id = session_id
|
|
57
|
+
self.status = status
|
|
58
|
+
self.proxy_config = proxy_config or {}
|
|
59
|
+
self.extra = kwargs
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nexalayer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: NexaLayer SDK — The Network Execution Layer for AI Agents
|
|
5
|
+
Author: NexaLayer
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Documentation, https://docs.nexalayer.net
|
|
8
|
+
Project-URL: Repository, https://github.com/NexaLayer/nexalayer-sdk
|
|
9
|
+
Project-URL: Issues, https://github.com/NexaLayer/nexalayer-sdk/issues
|
|
10
|
+
Keywords: proxy,agent,session,nexalayer
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: requests>=2.28
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
21
|
+
Requires-Dist: pytest-cov>=4; extra == "dev"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# NexaLayer SDK
|
|
25
|
+
|
|
26
|
+
Managed network sessions for Playwright, Browser Use, AI agents, and automation scripts.
|
|
27
|
+
|
|
28
|
+
**Keep using proxies. Stop managing them.**
|
|
29
|
+
中文:代理继续使用,代理管理交给 NexaLayer。
|
|
30
|
+
|
|
31
|
+
This repository contains the Python and Node/TypeScript SDK source. Release automation publishes signed release artifacts to PyPI and npm after registry ownership and secrets are configured.
|
|
32
|
+
|
|
33
|
+
## Status
|
|
34
|
+
|
|
35
|
+
| Runtime | Source | Package registry status |
|
|
36
|
+
| --- | --- | --- |
|
|
37
|
+
| Python | `python/` | Package name `nexalayer`; release workflow configured |
|
|
38
|
+
| Node / TypeScript | `node/` | Package name `@nexalayer/sdk`; release workflow configured |
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
Python from repository root:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
python3 -m venv .venv
|
|
46
|
+
. .venv/bin/activate
|
|
47
|
+
pip install -e ".[dev]"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Node from repository root:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
cd node
|
|
54
|
+
npm install
|
|
55
|
+
npm run build
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Authentication
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
export NEXALAYER_BASE_URL=https://api.nexalayer.net/v1
|
|
62
|
+
export NEXALAYER_API_KEY=agk_your_key
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The SDK uses `X-API-Key` for Agent requests.
|
|
66
|
+
|
|
67
|
+
## 60-second Python example
|
|
68
|
+
|
|
69
|
+
This example auto-selects a dynamic product, creates a session, reports telemetry, reads health, and terminates the session in cleanup.
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
import os
|
|
73
|
+
import time
|
|
74
|
+
import uuid
|
|
75
|
+
|
|
76
|
+
import requests
|
|
77
|
+
from nexalayer import NexaLayerClient
|
|
78
|
+
|
|
79
|
+
api_key = os.environ["NEXALAYER_API_KEY"]
|
|
80
|
+
client = NexaLayerClient(api_key=api_key)
|
|
81
|
+
session_id = None
|
|
82
|
+
|
|
83
|
+
def data(resp):
|
|
84
|
+
return resp.get("data", resp)
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
products = data(client.get_products(type="dynamic"))
|
|
88
|
+
product = next(p for p in products.get("items", []) if p.get("product_no"))
|
|
89
|
+
|
|
90
|
+
session = client.create_session(
|
|
91
|
+
session_type="dynamic",
|
|
92
|
+
product_no=product["product_no"],
|
|
93
|
+
quantity=1,
|
|
94
|
+
protocol="socks5",
|
|
95
|
+
rotation_mode="on_demand",
|
|
96
|
+
idempotency_key=f"py-{uuid.uuid4()}",
|
|
97
|
+
)
|
|
98
|
+
session_id = session.session_id
|
|
99
|
+
|
|
100
|
+
for _ in range(60):
|
|
101
|
+
current = data(client.get_session(session_id))
|
|
102
|
+
if current.get("status") == "active":
|
|
103
|
+
proxy_url = current["proxy"]["full_url"]
|
|
104
|
+
break
|
|
105
|
+
time.sleep(2)
|
|
106
|
+
else:
|
|
107
|
+
raise TimeoutError("Session did not become active")
|
|
108
|
+
|
|
109
|
+
res = requests.get(
|
|
110
|
+
"https://httpbin.org/ip",
|
|
111
|
+
proxies={"http": proxy_url, "https": proxy_url},
|
|
112
|
+
timeout=30,
|
|
113
|
+
)
|
|
114
|
+
client.report_event(
|
|
115
|
+
session_id,
|
|
116
|
+
event_type="success" if res.ok else "http_error",
|
|
117
|
+
status_code=res.status_code,
|
|
118
|
+
target_host="httpbin.org",
|
|
119
|
+
)
|
|
120
|
+
print(client.get_session_health(session_id))
|
|
121
|
+
finally:
|
|
122
|
+
if session_id:
|
|
123
|
+
client.terminate_session(session_id)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Dynamic Session
|
|
127
|
+
|
|
128
|
+
Use a dynamic session when your automation can rotate after errors such as timeout, 403, 429, or explicit blocks.
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
session = client.create_session(
|
|
132
|
+
session_type="dynamic",
|
|
133
|
+
product_no="selected_from_products_api",
|
|
134
|
+
quantity=1,
|
|
135
|
+
protocol="socks5",
|
|
136
|
+
rotation_mode="on_demand",
|
|
137
|
+
)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Static Session
|
|
141
|
+
|
|
142
|
+
Use a static session when you need fixed network identity for a longer-lived task.
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
session = client.create_session(
|
|
146
|
+
session_type="static",
|
|
147
|
+
product_no="selected_from_products_api",
|
|
148
|
+
quantity=1,
|
|
149
|
+
duration=30,
|
|
150
|
+
protocol="socks5",
|
|
151
|
+
)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Telemetry
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
client.report_event(
|
|
158
|
+
session_id,
|
|
159
|
+
event_type="timeout",
|
|
160
|
+
status_code=0,
|
|
161
|
+
target_host="example.com",
|
|
162
|
+
)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Health
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
health = client.get_session_health(session_id)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`timeout`, `captcha`, `block`, and repeated HTTP errors are not neutral noise; report them so NexaLayer can score the session and recommend rotate or pause.
|
|
172
|
+
|
|
173
|
+
## Cleanup
|
|
174
|
+
|
|
175
|
+
Always terminate sessions you no longer need:
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
try:
|
|
179
|
+
...
|
|
180
|
+
finally:
|
|
181
|
+
if session_id:
|
|
182
|
+
client.terminate_session(session_id)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Playwright
|
|
186
|
+
|
|
187
|
+
Use the public Playwright examples:
|
|
188
|
+
|
|
189
|
+
- Chinese: https://github.com/NexaLayer/nexalayer-examples/tree/main/playwright/basic-session
|
|
190
|
+
- English: https://github.com/NexaLayer/nexalayer-examples/tree/main/playwright/basic-session
|
|
191
|
+
|
|
192
|
+
## 中文文档
|
|
193
|
+
|
|
194
|
+
- 中文快速开始: https://docs.nexalayer.net/zh/quick-start
|
|
195
|
+
- Playwright 中文 Demo: https://docs.nexalayer.net/zh/examples
|
|
196
|
+
- API 文档: https://docs.nexalayer.net/api-reference/openapi
|
|
197
|
+
|
|
198
|
+
## Error handling
|
|
199
|
+
|
|
200
|
+
- Check `backend_ready=true` before creating paid sessions after registration.
|
|
201
|
+
- Use `Idempotency-Key` for create, rotate, renew, and recharge operations.
|
|
202
|
+
- Do not log `proxy.full_url` in production without redaction because it can contain proxy credentials.
|
|
203
|
+
- Respect target website terms and applicable law.
|
|
204
|
+
|
|
205
|
+
## Contributing
|
|
206
|
+
|
|
207
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
208
|
+
|
|
209
|
+
## Security
|
|
210
|
+
|
|
211
|
+
Do not open public issues with API keys, cookies, account passwords, proxy credentials, or target-site secrets. See [SECURITY.md](SECURITY.md).
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
python/nexalayer/__init__.py
|
|
5
|
+
python/nexalayer/client.py
|
|
6
|
+
python/nexalayer/errors.py
|
|
7
|
+
python/nexalayer/session.py
|
|
8
|
+
python/nexalayer/types.py
|
|
9
|
+
python/nexalayer.egg-info/PKG-INFO
|
|
10
|
+
python/nexalayer.egg-info/SOURCES.txt
|
|
11
|
+
python/nexalayer.egg-info/dependency_links.txt
|
|
12
|
+
python/nexalayer.egg-info/requires.txt
|
|
13
|
+
python/nexalayer.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nexalayer
|