qurl-python 0.2.1__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.
- layerv_qurl/__init__.py +161 -0
- layerv_qurl/_utils.py +1414 -0
- layerv_qurl/async_client.py +1512 -0
- layerv_qurl/client.py +1533 -0
- layerv_qurl/errors.py +152 -0
- layerv_qurl/langchain.py +159 -0
- layerv_qurl/py.typed +0 -0
- layerv_qurl/types.py +690 -0
- qurl_python-0.2.1.dist-info/METADATA +282 -0
- qurl_python-0.2.1.dist-info/RECORD +12 -0
- qurl_python-0.2.1.dist-info/WHEEL +4 -0
- qurl_python-0.2.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: qurl-python
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Python SDK for the qURL™ API — secure, time-limited access links. Quantum URL is how you enter the hidden layer of the internet.
|
|
5
|
+
Project-URL: Homepage, https://github.com/layervai/qurl-python
|
|
6
|
+
Project-URL: Repository, https://github.com/layervai/qurl-python
|
|
7
|
+
Project-URL: Documentation, https://docs.layerv.ai
|
|
8
|
+
Author-email: LayerV AI <engineering@layerv.ai>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ai-agents,layerv,qurl,security,zero-trust
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: httpx<1,>=0.27
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: langchain-core<2,>=0.3; extra == 'dev'
|
|
25
|
+
Requires-Dist: mypy>=1.14; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest-asyncio>=0.25; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
28
|
+
Requires-Dist: respx>=0.22; extra == 'dev'
|
|
29
|
+
Requires-Dist: ruff>=0.11; extra == 'dev'
|
|
30
|
+
Provides-Extra: langchain
|
|
31
|
+
Requires-Dist: langchain-core<2,>=0.3; extra == 'langchain'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# qurl-python
|
|
35
|
+
|
|
36
|
+
[](https://pypi.org/project/qurl-python/)
|
|
37
|
+
[](https://github.com/layervai/qurl-python/actions/workflows/ci.yml)
|
|
38
|
+
[](https://pypi.org/project/qurl-python/)
|
|
39
|
+
[](LICENSE)
|
|
40
|
+
|
|
41
|
+
Python SDK for the [qURL™ API](https://docs.layerv.ai) — secure, time-limited access links for AI agents.
|
|
42
|
+
|
|
43
|
+
> **Quantum URL (qURL)** · The internet has a hidden layer. This is how you enter.
|
|
44
|
+
|
|
45
|
+
## Why qURL?
|
|
46
|
+
|
|
47
|
+
AI agents need to access APIs, databases, and internal tools — but permanent credentials are a security risk. qURL creates **time-limited, auditable access links** that automatically expire:
|
|
48
|
+
|
|
49
|
+
- **Time-limited** — links expire after minutes, hours, or days
|
|
50
|
+
- **IP-scoped** — access is granted only to the requesting IP via NHP
|
|
51
|
+
- **Auditable** — every access is logged with who, when, and from where
|
|
52
|
+
- **Revocable** — kill access instantly if something goes wrong
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install qurl-python
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
For LangChain integration:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install qurl-python[langchain]
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Quick Start
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from layerv_qurl import QURLClient
|
|
70
|
+
|
|
71
|
+
client = QURLClient(api_key="lv_live_xxx")
|
|
72
|
+
|
|
73
|
+
# Create a protected link
|
|
74
|
+
result = client.create(
|
|
75
|
+
target_url="https://api.example.com/data",
|
|
76
|
+
expires_in="24h",
|
|
77
|
+
label="API access for agent",
|
|
78
|
+
)
|
|
79
|
+
print(result.qurl_link) # Share this link
|
|
80
|
+
|
|
81
|
+
# Resolve a token (grants network access for your IP)
|
|
82
|
+
access = client.resolve("at_k8xqp9h2sj9lx7r4a")
|
|
83
|
+
print(f"Access granted to {access.target_url} for {access.access_grant.expires_in}s")
|
|
84
|
+
|
|
85
|
+
# Extend a qURL's expiration
|
|
86
|
+
qurl = client.extend("r_xxx", "7d")
|
|
87
|
+
|
|
88
|
+
# Update resource metadata
|
|
89
|
+
qurl = client.update("r_xxx", description="extended", extend_by="7d")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Authentication Notes
|
|
93
|
+
|
|
94
|
+
`QURLClient(api_key=...)` accepts either a qURL API key or a JWT bearer token.
|
|
95
|
+
Dashboard/account endpoints such as billing, customer, connector, webhook, and
|
|
96
|
+
API-key management require JWT authentication. You may omit `api_key` only for
|
|
97
|
+
public endpoints such as access-code redemption; authenticated endpoints return
|
|
98
|
+
401 without credentials.
|
|
99
|
+
|
|
100
|
+
Some resource-list responses intentionally omit `target_url` for redacted
|
|
101
|
+
resource types. Treat `QURL.target_url` as `str | None` before formatting or
|
|
102
|
+
parsing it.
|
|
103
|
+
|
|
104
|
+
Mutating SDK methods generate a per-call `Idempotency-Key` when you do not
|
|
105
|
+
provide one and reuse it across the client's internal retries; qurl-service
|
|
106
|
+
supports that header on mutating endpoints, including `POST /v1/resolve`.
|
|
107
|
+
Automatic POST status-code retries remain limited to rate limits because
|
|
108
|
+
one-time resolve tokens can be consumed by server-side knock failures. Pass a
|
|
109
|
+
stable `idempotency_key` when you need retry-safe behavior across your own retry
|
|
110
|
+
loop, process restart, or job replay. Caller-supplied keys should be globally
|
|
111
|
+
unique for each logical operation; UUID or ULID values are recommended.
|
|
112
|
+
|
|
113
|
+
Fields such as webhook `events` and API-key `scopes` accept ordered non-string
|
|
114
|
+
iterables of strings. Lists, tuples, and generators preserve the caller's
|
|
115
|
+
iteration order; sets are rejected because their iteration order is not stable.
|
|
116
|
+
|
|
117
|
+
## Async Usage
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
import asyncio
|
|
121
|
+
from layerv_qurl import AsyncQURLClient
|
|
122
|
+
|
|
123
|
+
async def main():
|
|
124
|
+
async with AsyncQURLClient(api_key="lv_live_xxx") as client:
|
|
125
|
+
result = await client.create(target_url="https://example.com", expires_in="1h")
|
|
126
|
+
access = await client.resolve("at_...")
|
|
127
|
+
|
|
128
|
+
# Extend expiration
|
|
129
|
+
qurl = await client.extend("r_xxx", "7d")
|
|
130
|
+
|
|
131
|
+
asyncio.run(main())
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Pagination
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
# Iterate all active qURLs (auto-paginates)
|
|
138
|
+
for qurl in client.list_all(status="active"):
|
|
139
|
+
target = qurl.target_url or "<redacted>"
|
|
140
|
+
print(f"{qurl.resource_id}: {target}")
|
|
141
|
+
|
|
142
|
+
# Or fetch a single page
|
|
143
|
+
page = client.list(status="active", limit=10)
|
|
144
|
+
for qurl in page.qurls:
|
|
145
|
+
print(qurl.resource_id)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Resources
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
# Create a resource explicitly, then mint scoped qURLs against it
|
|
152
|
+
resource = client.create_resource(
|
|
153
|
+
resource_type="url",
|
|
154
|
+
target_url="https://api.example.com/data",
|
|
155
|
+
alias="reports-api",
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
link = client.create_qurl_for_resource(
|
|
159
|
+
resource.resource_id,
|
|
160
|
+
expires_in="1h",
|
|
161
|
+
label="Alice from Acme",
|
|
162
|
+
idempotency_key="invite-alice-2026-03-10",
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Revoke one token without closing the whole resource
|
|
166
|
+
assert link.qurl_id is not None
|
|
167
|
+
client.revoke_resource_qurl(resource.resource_id, link.qurl_id)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Custom Domains And Webhooks
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
domain = client.register_domain("secure.example.com")
|
|
174
|
+
for record in domain.dns_records:
|
|
175
|
+
print(record.type, record.name, record.value)
|
|
176
|
+
|
|
177
|
+
webhook = client.create_webhook(
|
|
178
|
+
url="https://example.com/qurl-webhooks",
|
|
179
|
+
events=["qurl.accessed", "domain.verified"],
|
|
180
|
+
)
|
|
181
|
+
print(webhook.secret) # Returned only on create/regenerate
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Error Handling
|
|
185
|
+
|
|
186
|
+
Every API error maps to a specific exception class, so you can catch exactly what you need:
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
from layerv_qurl import (
|
|
190
|
+
QURLClient,
|
|
191
|
+
QURLError,
|
|
192
|
+
QURLNetworkError,
|
|
193
|
+
QURLTimeoutError,
|
|
194
|
+
)
|
|
195
|
+
from layerv_qurl.errors import (
|
|
196
|
+
AuthenticationError,
|
|
197
|
+
AuthorizationError,
|
|
198
|
+
NotFoundError,
|
|
199
|
+
RateLimitError,
|
|
200
|
+
ValidationError,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
client = QURLClient(api_key="lv_live_xxx")
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
client.resolve("at_k8xqp9h2sj9lx7r4a")
|
|
207
|
+
except AuthenticationError:
|
|
208
|
+
print("Bad API key")
|
|
209
|
+
except AuthorizationError:
|
|
210
|
+
print("Valid key but missing qurl:resolve scope")
|
|
211
|
+
except NotFoundError:
|
|
212
|
+
print("Token doesn't exist or already expired")
|
|
213
|
+
except RateLimitError as e:
|
|
214
|
+
print(f"Rate limited — retry in {e.retry_after}s")
|
|
215
|
+
except ValidationError as e:
|
|
216
|
+
print(f"Bad request: {e.detail}")
|
|
217
|
+
if e.invalid_fields:
|
|
218
|
+
for field, reason in e.invalid_fields.items():
|
|
219
|
+
print(f" {field}: {reason}")
|
|
220
|
+
except QURLTimeoutError:
|
|
221
|
+
print("Request timed out")
|
|
222
|
+
except QURLNetworkError as e:
|
|
223
|
+
print(f"Network error: {e}")
|
|
224
|
+
except QURLError as e:
|
|
225
|
+
# Catch-all for any other API error
|
|
226
|
+
print(f"API error {e.status}: {e.detail}")
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
All error classes inherit from `QURLError`, so `except QURLError` catches everything.
|
|
230
|
+
|
|
231
|
+
## Typed Quota
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
quota = client.get_quota()
|
|
235
|
+
print(f"Plan: {quota.plan}")
|
|
236
|
+
print(f"Active qURLs: {quota.usage.active_qurls}")
|
|
237
|
+
print(f"Rate limit: {quota.rate_limits.create_per_minute}/min")
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
JWT-authenticated dashboard endpoints are also available for usage, customer
|
|
241
|
+
settings, billing sessions, invoices, and API-key management. API-key auth
|
|
242
|
+
continues to work for normal qURL, resource, domain, webhook, connector, and
|
|
243
|
+
access-code operations according to the API scopes on the key.
|
|
244
|
+
|
|
245
|
+
## Debug Logging
|
|
246
|
+
|
|
247
|
+
Enable debug logs to see every request and retry:
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
import logging
|
|
251
|
+
logging.getLogger("layerv_qurl").setLevel(logging.DEBUG)
|
|
252
|
+
|
|
253
|
+
# Output:
|
|
254
|
+
# DEBUG:layerv_qurl:POST https://api.layerv.ai/v1/qurl
|
|
255
|
+
# DEBUG:layerv_qurl:POST https://api.layerv.ai/v1/qurl → 201
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## LangChain Integration
|
|
259
|
+
|
|
260
|
+
```python
|
|
261
|
+
from layerv_qurl import QURLClient
|
|
262
|
+
from layerv_qurl.langchain import QURLToolkit
|
|
263
|
+
|
|
264
|
+
client = QURLClient(api_key="lv_live_xxx")
|
|
265
|
+
toolkit = QURLToolkit(client=client)
|
|
266
|
+
tools = toolkit.get_tools() # [CreateQURLTool, ResolveQURLTool, ListQURLsTool, DeleteQURLTool]
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Configuration
|
|
270
|
+
|
|
271
|
+
| Parameter | Required | Default |
|
|
272
|
+
|-----------|----------|---------|
|
|
273
|
+
| `api_key` | Yes | — |
|
|
274
|
+
| `base_url` | No | `https://api.layerv.ai` |
|
|
275
|
+
| `timeout` | No | `30.0` |
|
|
276
|
+
| `max_retries` | No | `3` |
|
|
277
|
+
| `user_agent` | No | `qurl-python-sdk/<version>` |
|
|
278
|
+
| `http_client` | No | Auto-created `httpx.Client` |
|
|
279
|
+
|
|
280
|
+
## License
|
|
281
|
+
|
|
282
|
+
MIT
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
layerv_qurl/__init__.py,sha256=T9x8M_JH8nCRoGx_jRUmMVunzVT8HnXjrNkAEc8WZKc,3466
|
|
2
|
+
layerv_qurl/_utils.py,sha256=GHGvouoaiU7AOTxH1fHBicl0c0PKrxu2SCMfSY2EWRk,53117
|
|
3
|
+
layerv_qurl/async_client.py,sha256=vOVkGA7jWU_pISRyYBWt1HUwyQtO4LjQhtJicysbXwA,56683
|
|
4
|
+
layerv_qurl/client.py,sha256=Pud7NBREo-Dy9jt74-TUM73MWQPhjJOB5Id7Zgu5sJ0,56716
|
|
5
|
+
layerv_qurl/errors.py,sha256=7vyBEsJnN-e7804hil9KRwEgmUv5QW-DUGTf6FdSaV0,4932
|
|
6
|
+
layerv_qurl/langchain.py,sha256=MJdow63lc6DXfL-riyu-rYC3cMiEKIOY9UZUvY4hEUQ,4695
|
|
7
|
+
layerv_qurl/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
layerv_qurl/types.py,sha256=5T2rRec2-rgG6MTAEJHALrKNQEgXvGnZfP32q1BC-8M,17515
|
|
9
|
+
qurl_python-0.2.1.dist-info/METADATA,sha256=NyvktzPpY02XkgsgGxIYM0_r0x4Pt_gvCohTt8QZLno,9050
|
|
10
|
+
qurl_python-0.2.1.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
11
|
+
qurl_python-0.2.1.dist-info/licenses/LICENSE,sha256=cg6W6pLqMQFf89yPfz5ClnVSrNEeoTh3NOEQyH9CaZw,1069
|
|
12
|
+
qurl_python-0.2.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 LayerV, Inc.
|
|
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.
|