deltex 1.3.1__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.
- deltex-1.3.1/.gitignore +35 -0
- deltex-1.3.1/PKG-INFO +207 -0
- deltex-1.3.1/README.md +196 -0
- deltex-1.3.1/deltex/__init__.py +35 -0
- deltex-1.3.1/deltex/async_client.py +215 -0
- deltex-1.3.1/deltex/cli.py +686 -0
- deltex-1.3.1/deltex/client.py +382 -0
- deltex-1.3.1/integration_test.py +210 -0
- deltex-1.3.1/pyproject.toml +23 -0
deltex-1.3.1/.gitignore
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/target
|
|
2
|
+
**/*.rs.bk
|
|
3
|
+
/bin
|
|
4
|
+
/pkg
|
|
5
|
+
*.wasm
|
|
6
|
+
*.tar.gzclaudecode-linux-x64
|
|
7
|
+
edgedb-docs.zip
|
|
8
|
+
fastly-api.toml
|
|
9
|
+
claudecode-linux-x64
|
|
10
|
+
|
|
11
|
+
# Web build artifacts
|
|
12
|
+
web/node_modules/
|
|
13
|
+
web/dist/
|
|
14
|
+
redis.env
|
|
15
|
+
*.pem
|
|
16
|
+
dashboard/node_modules/
|
|
17
|
+
dashboard/dist/
|
|
18
|
+
dashboard/.env.local
|
|
19
|
+
dashboard/test-results/
|
|
20
|
+
benchmarks/__pycache__/
|
|
21
|
+
|
|
22
|
+
# SDK build artifacts
|
|
23
|
+
sdk/rust/target/
|
|
24
|
+
sdk/typescript/node_modules/
|
|
25
|
+
sdk/typescript/dist/
|
|
26
|
+
sdk/typescript/build/
|
|
27
|
+
sdk/python/__pycache__/
|
|
28
|
+
sdk/python/*.egg-info/
|
|
29
|
+
sdk/python/build/
|
|
30
|
+
sdk/python/dist/
|
|
31
|
+
sdk/python/.eggs/
|
|
32
|
+
sdk/python/**/__pycache__/
|
|
33
|
+
sdk/go/bin/
|
|
34
|
+
sdk/go/vendor/
|
|
35
|
+
sdk/rust/Cargo.lock
|
deltex-1.3.1/PKG-INFO
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: deltex
|
|
3
|
+
Version: 1.3.1
|
|
4
|
+
Summary: Official Python client for Deltex — edge-native SQL database
|
|
5
|
+
Project-URL: Homepage, https://deltex.dev
|
|
6
|
+
Project-URL: Repository, https://github.com/deltex/deltex-python
|
|
7
|
+
License: MIT
|
|
8
|
+
Keywords: database,deltex,edge,sql
|
|
9
|
+
Requires-Python: >=3.8
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# deltex — Python client
|
|
13
|
+
|
|
14
|
+
Official Python client for [Deltex](https://deltex.dev) — edge-native SQL database.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install deltex
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick start
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
import deltex
|
|
26
|
+
|
|
27
|
+
# Auto-reads DELTEX_API_KEY from environment
|
|
28
|
+
db = deltex.connect()
|
|
29
|
+
|
|
30
|
+
# Query
|
|
31
|
+
users = db.query("SELECT * FROM users WHERE active = $1", [True])
|
|
32
|
+
user = db.query_one("SELECT * FROM users WHERE id = $1", [42])
|
|
33
|
+
|
|
34
|
+
# Mutation
|
|
35
|
+
n = db.execute("INSERT INTO events (type, ts) VALUES ($1, NOW())", ["pageview"])
|
|
36
|
+
|
|
37
|
+
# Full result with commit status
|
|
38
|
+
result = db.execute_raw("INSERT INTO orders (amount) VALUES ($1)", [99.99])
|
|
39
|
+
print(result.commit_status) # "edge-accepted" | "committed"
|
|
40
|
+
print(result.execution_ms) # server-side execution time
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## API
|
|
44
|
+
|
|
45
|
+
### `deltex.connect(api_key=None, endpoint=None, write_mode="sync", ...)`
|
|
46
|
+
|
|
47
|
+
| Param | Default | Description |
|
|
48
|
+
|-------|---------|-------------|
|
|
49
|
+
| `api_key` | `DELTEX_API_KEY` env | Bearer token |
|
|
50
|
+
| `endpoint` | `DELTEX_ENDPOINT` or `https://db.deltex.dev` | Engine URL |
|
|
51
|
+
| `write_mode` | `"sync"` | `"sync"` / `"edge"` / `"async"` |
|
|
52
|
+
| `timeout` | `30.0` | Request timeout (seconds) |
|
|
53
|
+
| `max_retries` | `3` | Auto-retry on 429 |
|
|
54
|
+
| `tag` | `None` | X-Query-Tag for analytics |
|
|
55
|
+
|
|
56
|
+
### Methods
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
db.query(sql, params=[]) → list[dict]
|
|
60
|
+
db.query_one(sql, params=[]) → dict | None
|
|
61
|
+
db.execute(sql, params=[]) → int (rows affected)
|
|
62
|
+
db.execute_raw(sql, params=[]) → QueryResult
|
|
63
|
+
|
|
64
|
+
db.transaction(fn) # BEGIN → fn(tx) → COMMIT
|
|
65
|
+
db.batch(statements) # atomic multi-statement, one round-trip → int
|
|
66
|
+
|
|
67
|
+
db.with_write_mode("sync") → Client (per-client write mode)
|
|
68
|
+
db.strong → Client (X-Consistency: strong)
|
|
69
|
+
db.with_idempotency_key(key) → Client (safe retry deduplication)
|
|
70
|
+
db.with_tag(tag) → Client (X-Query-Tag)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Async
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
import asyncio
|
|
77
|
+
import deltex
|
|
78
|
+
|
|
79
|
+
async def main():
|
|
80
|
+
async with deltex.async_connect() as db:
|
|
81
|
+
users = await db.query("SELECT * FROM users LIMIT 10")
|
|
82
|
+
print(users)
|
|
83
|
+
|
|
84
|
+
asyncio.run(main())
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Transaction
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
def transfer(tx):
|
|
91
|
+
tx.execute("UPDATE accounts SET balance = balance - $1 WHERE id = $2", [100, 1])
|
|
92
|
+
tx.execute("UPDATE accounts SET balance = balance + $1 WHERE id = $2", [100, 2])
|
|
93
|
+
|
|
94
|
+
db.transaction(transfer)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Batch — fastest bulk write
|
|
98
|
+
|
|
99
|
+
`batch()` applies a list of SQL statements in **one round-trip**, committed
|
|
100
|
+
atomically, and returns total rows affected:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
n = db.batch([
|
|
104
|
+
"INSERT INTO products (name, price) VALUES ('Apple', 0.99)",
|
|
105
|
+
"INSERT INTO products (name, price) VALUES ('Banana', 0.59)",
|
|
106
|
+
]) # n == 2
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Each durable write has a ~300ms Fastly-KV commit floor, so looping `execute`
|
|
110
|
+
costs ~N × 300ms. `batch()` (and a single multi-row `INSERT`) coalesce into one
|
|
111
|
+
durable commit — O(1) instead of O(N): 78 separate writes ≈ 54s, the same 78 in
|
|
112
|
+
a `batch()` ≈ 2s. `batch()` takes raw SQL (no binding) — for untrusted values,
|
|
113
|
+
build statements safely or use `transaction`.
|
|
114
|
+
|
|
115
|
+
## CLI
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
export DELTEX_API_KEY="dtx_k_..."
|
|
119
|
+
|
|
120
|
+
deltex query "SELECT * FROM users LIMIT 5"
|
|
121
|
+
deltex tables
|
|
122
|
+
deltex schema orders
|
|
123
|
+
deltex exec "DELETE FROM sessions WHERE expires_at < NOW()"
|
|
124
|
+
deltex health
|
|
125
|
+
deltex bench --samples 30
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Write Modes
|
|
129
|
+
|
|
130
|
+
| Mode | Latency | Use when |
|
|
131
|
+
|------|---------|----------|
|
|
132
|
+
| `sync` (default) | ~340ms | Everything by default; durable, never loses an acked write (batch to amortize) |
|
|
133
|
+
| `edge` | ~10ms | Caches, sessions, idempotent upserts — eventual durability |
|
|
134
|
+
| `async` | ~5ms | High-volume telemetry, fire-and-forget |
|
|
135
|
+
|
|
136
|
+
## Error handling
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
from deltex import DeltexError, RateLimitError
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
db.query("SELECT * FROM nonexistent")
|
|
143
|
+
except RateLimitError as e:
|
|
144
|
+
print(f"Rate limited, retry after {e.retry_after}s")
|
|
145
|
+
except DeltexError as e:
|
|
146
|
+
print(f"Error {e.status}: {e.engine_message}")
|
|
147
|
+
print(f"SQL: {e.sql}")
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
MIT
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Common Patterns
|
|
157
|
+
|
|
158
|
+
### Error handling
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
import time
|
|
162
|
+
from deltex import connect, RateLimitError, DeltexError
|
|
163
|
+
|
|
164
|
+
db = connect()
|
|
165
|
+
try:
|
|
166
|
+
result = db.query("SELECT * FROM users WHERE id = $1", [42])
|
|
167
|
+
except RateLimitError as e:
|
|
168
|
+
time.sleep(e.retry_after)
|
|
169
|
+
result = db.query("SELECT * FROM users WHERE id = $1", [42])
|
|
170
|
+
except DeltexError as e:
|
|
171
|
+
print(f"Query failed: {e}")
|
|
172
|
+
raise
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Async client (non-blocking)
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
import deltex
|
|
179
|
+
|
|
180
|
+
async def main():
|
|
181
|
+
db = deltex.async_connect()
|
|
182
|
+
rows = await db.query("SELECT * FROM products WHERE price < $1", [50.0])
|
|
183
|
+
for row in rows:
|
|
184
|
+
print(row["name"], row["price"])
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### CLI usage
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
# Run a query
|
|
191
|
+
deltex query "SELECT COUNT(*) FROM users"
|
|
192
|
+
|
|
193
|
+
# Migrate
|
|
194
|
+
deltex migrate --file migrations/001_create_users.sql
|
|
195
|
+
|
|
196
|
+
# Backup
|
|
197
|
+
deltex backup --output backup.json
|
|
198
|
+
|
|
199
|
+
# Manage API keys
|
|
200
|
+
deltex keys list
|
|
201
|
+
deltex keys create --name "production"
|
|
202
|
+
deltex keys revoke --id key_id_here
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## SDK Version
|
|
206
|
+
|
|
207
|
+
`v1.3.1` — see [CHANGELOG.md](../../CHANGELOG.md) for history.
|
deltex-1.3.1/README.md
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# deltex — Python client
|
|
2
|
+
|
|
3
|
+
Official Python client for [Deltex](https://deltex.dev) — edge-native SQL database.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install deltex
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
import deltex
|
|
15
|
+
|
|
16
|
+
# Auto-reads DELTEX_API_KEY from environment
|
|
17
|
+
db = deltex.connect()
|
|
18
|
+
|
|
19
|
+
# Query
|
|
20
|
+
users = db.query("SELECT * FROM users WHERE active = $1", [True])
|
|
21
|
+
user = db.query_one("SELECT * FROM users WHERE id = $1", [42])
|
|
22
|
+
|
|
23
|
+
# Mutation
|
|
24
|
+
n = db.execute("INSERT INTO events (type, ts) VALUES ($1, NOW())", ["pageview"])
|
|
25
|
+
|
|
26
|
+
# Full result with commit status
|
|
27
|
+
result = db.execute_raw("INSERT INTO orders (amount) VALUES ($1)", [99.99])
|
|
28
|
+
print(result.commit_status) # "edge-accepted" | "committed"
|
|
29
|
+
print(result.execution_ms) # server-side execution time
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## API
|
|
33
|
+
|
|
34
|
+
### `deltex.connect(api_key=None, endpoint=None, write_mode="sync", ...)`
|
|
35
|
+
|
|
36
|
+
| Param | Default | Description |
|
|
37
|
+
|-------|---------|-------------|
|
|
38
|
+
| `api_key` | `DELTEX_API_KEY` env | Bearer token |
|
|
39
|
+
| `endpoint` | `DELTEX_ENDPOINT` or `https://db.deltex.dev` | Engine URL |
|
|
40
|
+
| `write_mode` | `"sync"` | `"sync"` / `"edge"` / `"async"` |
|
|
41
|
+
| `timeout` | `30.0` | Request timeout (seconds) |
|
|
42
|
+
| `max_retries` | `3` | Auto-retry on 429 |
|
|
43
|
+
| `tag` | `None` | X-Query-Tag for analytics |
|
|
44
|
+
|
|
45
|
+
### Methods
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
db.query(sql, params=[]) → list[dict]
|
|
49
|
+
db.query_one(sql, params=[]) → dict | None
|
|
50
|
+
db.execute(sql, params=[]) → int (rows affected)
|
|
51
|
+
db.execute_raw(sql, params=[]) → QueryResult
|
|
52
|
+
|
|
53
|
+
db.transaction(fn) # BEGIN → fn(tx) → COMMIT
|
|
54
|
+
db.batch(statements) # atomic multi-statement, one round-trip → int
|
|
55
|
+
|
|
56
|
+
db.with_write_mode("sync") → Client (per-client write mode)
|
|
57
|
+
db.strong → Client (X-Consistency: strong)
|
|
58
|
+
db.with_idempotency_key(key) → Client (safe retry deduplication)
|
|
59
|
+
db.with_tag(tag) → Client (X-Query-Tag)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Async
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
import asyncio
|
|
66
|
+
import deltex
|
|
67
|
+
|
|
68
|
+
async def main():
|
|
69
|
+
async with deltex.async_connect() as db:
|
|
70
|
+
users = await db.query("SELECT * FROM users LIMIT 10")
|
|
71
|
+
print(users)
|
|
72
|
+
|
|
73
|
+
asyncio.run(main())
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Transaction
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
def transfer(tx):
|
|
80
|
+
tx.execute("UPDATE accounts SET balance = balance - $1 WHERE id = $2", [100, 1])
|
|
81
|
+
tx.execute("UPDATE accounts SET balance = balance + $1 WHERE id = $2", [100, 2])
|
|
82
|
+
|
|
83
|
+
db.transaction(transfer)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Batch — fastest bulk write
|
|
87
|
+
|
|
88
|
+
`batch()` applies a list of SQL statements in **one round-trip**, committed
|
|
89
|
+
atomically, and returns total rows affected:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
n = db.batch([
|
|
93
|
+
"INSERT INTO products (name, price) VALUES ('Apple', 0.99)",
|
|
94
|
+
"INSERT INTO products (name, price) VALUES ('Banana', 0.59)",
|
|
95
|
+
]) # n == 2
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Each durable write has a ~300ms Fastly-KV commit floor, so looping `execute`
|
|
99
|
+
costs ~N × 300ms. `batch()` (and a single multi-row `INSERT`) coalesce into one
|
|
100
|
+
durable commit — O(1) instead of O(N): 78 separate writes ≈ 54s, the same 78 in
|
|
101
|
+
a `batch()` ≈ 2s. `batch()` takes raw SQL (no binding) — for untrusted values,
|
|
102
|
+
build statements safely or use `transaction`.
|
|
103
|
+
|
|
104
|
+
## CLI
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
export DELTEX_API_KEY="dtx_k_..."
|
|
108
|
+
|
|
109
|
+
deltex query "SELECT * FROM users LIMIT 5"
|
|
110
|
+
deltex tables
|
|
111
|
+
deltex schema orders
|
|
112
|
+
deltex exec "DELETE FROM sessions WHERE expires_at < NOW()"
|
|
113
|
+
deltex health
|
|
114
|
+
deltex bench --samples 30
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Write Modes
|
|
118
|
+
|
|
119
|
+
| Mode | Latency | Use when |
|
|
120
|
+
|------|---------|----------|
|
|
121
|
+
| `sync` (default) | ~340ms | Everything by default; durable, never loses an acked write (batch to amortize) |
|
|
122
|
+
| `edge` | ~10ms | Caches, sessions, idempotent upserts — eventual durability |
|
|
123
|
+
| `async` | ~5ms | High-volume telemetry, fire-and-forget |
|
|
124
|
+
|
|
125
|
+
## Error handling
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from deltex import DeltexError, RateLimitError
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
db.query("SELECT * FROM nonexistent")
|
|
132
|
+
except RateLimitError as e:
|
|
133
|
+
print(f"Rate limited, retry after {e.retry_after}s")
|
|
134
|
+
except DeltexError as e:
|
|
135
|
+
print(f"Error {e.status}: {e.engine_message}")
|
|
136
|
+
print(f"SQL: {e.sql}")
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
|
|
141
|
+
MIT
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Common Patterns
|
|
146
|
+
|
|
147
|
+
### Error handling
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
import time
|
|
151
|
+
from deltex import connect, RateLimitError, DeltexError
|
|
152
|
+
|
|
153
|
+
db = connect()
|
|
154
|
+
try:
|
|
155
|
+
result = db.query("SELECT * FROM users WHERE id = $1", [42])
|
|
156
|
+
except RateLimitError as e:
|
|
157
|
+
time.sleep(e.retry_after)
|
|
158
|
+
result = db.query("SELECT * FROM users WHERE id = $1", [42])
|
|
159
|
+
except DeltexError as e:
|
|
160
|
+
print(f"Query failed: {e}")
|
|
161
|
+
raise
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Async client (non-blocking)
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
import deltex
|
|
168
|
+
|
|
169
|
+
async def main():
|
|
170
|
+
db = deltex.async_connect()
|
|
171
|
+
rows = await db.query("SELECT * FROM products WHERE price < $1", [50.0])
|
|
172
|
+
for row in rows:
|
|
173
|
+
print(row["name"], row["price"])
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### CLI usage
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
# Run a query
|
|
180
|
+
deltex query "SELECT COUNT(*) FROM users"
|
|
181
|
+
|
|
182
|
+
# Migrate
|
|
183
|
+
deltex migrate --file migrations/001_create_users.sql
|
|
184
|
+
|
|
185
|
+
# Backup
|
|
186
|
+
deltex backup --output backup.json
|
|
187
|
+
|
|
188
|
+
# Manage API keys
|
|
189
|
+
deltex keys list
|
|
190
|
+
deltex keys create --name "production"
|
|
191
|
+
deltex keys revoke --id key_id_here
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## SDK Version
|
|
195
|
+
|
|
196
|
+
`v1.3.1` — see [CHANGELOG.md](../../CHANGELOG.md) for history.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
deltex — Official Python client for Deltex edge-native SQL database.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
import deltex
|
|
6
|
+
|
|
7
|
+
db = deltex.connect() # reads DELTEX_API_KEY from env
|
|
8
|
+
|
|
9
|
+
# Query
|
|
10
|
+
users = db.query("SELECT * FROM users WHERE active = $1", [True])
|
|
11
|
+
user = db.query_one("SELECT * FROM users WHERE id = $1", [42])
|
|
12
|
+
|
|
13
|
+
# Execute (INSERT/UPDATE/DELETE)
|
|
14
|
+
n = db.execute("INSERT INTO events (type, ts) VALUES ($1, NOW())", ["click"])
|
|
15
|
+
|
|
16
|
+
# Async
|
|
17
|
+
import asyncio
|
|
18
|
+
async def main():
|
|
19
|
+
async with deltex.async_connect() as db:
|
|
20
|
+
users = await db.query("SELECT * FROM users")
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from .client import connect, Client, DeltexError, RateLimitError, QueryResult
|
|
24
|
+
from .async_client import async_connect, AsyncClient
|
|
25
|
+
|
|
26
|
+
__version__ = "1.3.1"
|
|
27
|
+
__all__ = [
|
|
28
|
+
"connect",
|
|
29
|
+
"async_connect",
|
|
30
|
+
"Client",
|
|
31
|
+
"AsyncClient",
|
|
32
|
+
"DeltexError",
|
|
33
|
+
"RateLimitError",
|
|
34
|
+
"QueryResult",
|
|
35
|
+
]
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Deltex asyncio client.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import json
|
|
9
|
+
import asyncio
|
|
10
|
+
import urllib.request
|
|
11
|
+
import urllib.error
|
|
12
|
+
from typing import Any, Dict, List, Optional, Sequence
|
|
13
|
+
|
|
14
|
+
from .client import (
|
|
15
|
+
_bind, _format_param, _TIMING_RE, _COMMIT_STATUS_VALUES,
|
|
16
|
+
DeltexError, RateLimitError, QueryResult, Row, Param,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def _run_query_async(
|
|
21
|
+
sql: str,
|
|
22
|
+
url: str,
|
|
23
|
+
headers: Dict[str, str],
|
|
24
|
+
timeout: float,
|
|
25
|
+
max_retries: int,
|
|
26
|
+
) -> QueryResult:
|
|
27
|
+
"""Async HTTP execution using urllib in a thread (no external deps)."""
|
|
28
|
+
from .client import _run_query
|
|
29
|
+
loop = asyncio.get_event_loop()
|
|
30
|
+
return await loop.run_in_executor(
|
|
31
|
+
None,
|
|
32
|
+
lambda: _run_query(sql, url, headers, timeout, max_retries),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AsyncClient:
|
|
37
|
+
"""
|
|
38
|
+
Deltex asyncio SQL client.
|
|
39
|
+
|
|
40
|
+
Example:
|
|
41
|
+
async with deltex.async_connect() as db:
|
|
42
|
+
users = await db.query("SELECT * FROM users WHERE active = $1", [True])
|
|
43
|
+
user = await db.query_one("SELECT * FROM users WHERE id = $1", [42])
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
api_key: Optional[str] = None,
|
|
49
|
+
endpoint: Optional[str] = None,
|
|
50
|
+
write_mode: str = "sync",
|
|
51
|
+
timeout: float = 30.0,
|
|
52
|
+
max_retries: int = 3,
|
|
53
|
+
tag: Optional[str] = None,
|
|
54
|
+
):
|
|
55
|
+
self._api_key = api_key or os.environ.get("DELTEX_API_KEY", "")
|
|
56
|
+
if not self._api_key:
|
|
57
|
+
raise DeltexError("No API key. Set DELTEX_API_KEY env var or pass api_key=")
|
|
58
|
+
|
|
59
|
+
ep = (endpoint or os.environ.get("DELTEX_ENDPOINT") or "https://db.deltex.dev").rstrip("/")
|
|
60
|
+
self._url = f"{ep}/v1/query"
|
|
61
|
+
self._write_mode = write_mode
|
|
62
|
+
self._timeout = timeout
|
|
63
|
+
self._max_retries = max_retries
|
|
64
|
+
|
|
65
|
+
self._headers: Dict[str, str] = {
|
|
66
|
+
"Content-Type": "application/json",
|
|
67
|
+
"Authorization": f"Bearer {self._api_key}",
|
|
68
|
+
"X-Write-Mode": write_mode,
|
|
69
|
+
}
|
|
70
|
+
if tag:
|
|
71
|
+
self._headers["X-Query-Tag"] = tag
|
|
72
|
+
|
|
73
|
+
async def __aenter__(self) -> "AsyncClient":
|
|
74
|
+
return self
|
|
75
|
+
|
|
76
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
async def query(self, sql: str, params: Sequence[Param] = ()) -> List[Row]:
|
|
80
|
+
"""Execute SQL, return all rows."""
|
|
81
|
+
result = await _run_query_async(_bind(sql, params), self._url, self._headers, self._timeout, self._max_retries)
|
|
82
|
+
return result.rows
|
|
83
|
+
|
|
84
|
+
async def query_one(self, sql: str, params: Sequence[Param] = ()) -> Optional[Row]:
|
|
85
|
+
"""Execute SQL, return first row or None."""
|
|
86
|
+
rows = (await _run_query_async(_bind(sql, params), self._url, self._headers, self._timeout, self._max_retries)).rows
|
|
87
|
+
return rows[0] if rows else None
|
|
88
|
+
|
|
89
|
+
async def execute(self, sql: str, params: Sequence[Param] = ()) -> int:
|
|
90
|
+
"""Execute a mutation, return rows affected."""
|
|
91
|
+
return (await _run_query_async(_bind(sql, params), self._url, self._headers, self._timeout, self._max_retries)).rows_affected
|
|
92
|
+
|
|
93
|
+
async def execute_raw(self, sql: str, params: Sequence[Param] = ()) -> QueryResult:
|
|
94
|
+
"""Execute SQL, return full QueryResult."""
|
|
95
|
+
return await _run_query_async(_bind(sql, params), self._url, self._headers, self._timeout, self._max_retries)
|
|
96
|
+
|
|
97
|
+
async def transaction(self, fn):
|
|
98
|
+
"""
|
|
99
|
+
Execute an atomic transaction via Deltex's /transaction endpoint.
|
|
100
|
+
|
|
101
|
+
Mutating statements (``execute``/``execute_raw``) are collected and sent
|
|
102
|
+
atomically in a single round-trip; reads (``query``/``query_one``) execute
|
|
103
|
+
live. If ``fn`` raises before returning, no statements are sent — the
|
|
104
|
+
transaction is effectively rolled back.
|
|
105
|
+
|
|
106
|
+
Example:
|
|
107
|
+
async def do_transfer(tx):
|
|
108
|
+
await tx.execute("UPDATE accounts SET balance = balance - $1 WHERE id = $2", [100, 1])
|
|
109
|
+
await tx.execute("UPDATE accounts SET balance = balance + $1 WHERE id = $2", [100, 2])
|
|
110
|
+
|
|
111
|
+
await db.transaction(do_transfer)
|
|
112
|
+
"""
|
|
113
|
+
statements: list = []
|
|
114
|
+
outer = self
|
|
115
|
+
|
|
116
|
+
class CollectingClient:
|
|
117
|
+
async def query(self_inner, sql: str, params: Sequence[Param] = ()) -> List[Row]:
|
|
118
|
+
return await outer.query(sql, params)
|
|
119
|
+
|
|
120
|
+
async def query_one(self_inner, sql: str, params: Sequence[Param] = ()) -> Optional[Row]:
|
|
121
|
+
return await outer.query_one(sql, params)
|
|
122
|
+
|
|
123
|
+
async def execute(self_inner, sql: str, params: Sequence[Param] = ()) -> int:
|
|
124
|
+
statements.append(_bind(sql, params))
|
|
125
|
+
return 0
|
|
126
|
+
|
|
127
|
+
async def execute_raw(self_inner, sql: str, params: Sequence[Param] = ()) -> QueryResult:
|
|
128
|
+
statements.append(_bind(sql, params))
|
|
129
|
+
return QueryResult(rows=[], columns=[], rows_affected=0)
|
|
130
|
+
|
|
131
|
+
tx = CollectingClient()
|
|
132
|
+
user_result = await fn(tx)
|
|
133
|
+
|
|
134
|
+
if not statements:
|
|
135
|
+
return user_result
|
|
136
|
+
|
|
137
|
+
tx_url = self._url.replace("/v1/query", "/v1/transaction")
|
|
138
|
+
body = json.dumps({"statements": statements, "isolation": "SERIALIZABLE"}).encode()
|
|
139
|
+
|
|
140
|
+
def _send() -> None:
|
|
141
|
+
req = urllib.request.Request(tx_url, data=body, method="POST", headers=self._headers)
|
|
142
|
+
try:
|
|
143
|
+
with urllib.request.urlopen(req, timeout=self._timeout) as resp:
|
|
144
|
+
data = json.loads(resp.read())
|
|
145
|
+
except urllib.error.HTTPError as e:
|
|
146
|
+
data = json.loads(e.read())
|
|
147
|
+
raise DeltexError(data.get("message", str(e)), e.code, "; ".join(statements))
|
|
148
|
+
if data.get("success") is False:
|
|
149
|
+
raise DeltexError(data.get("message", "Transaction failed"), 500, "; ".join(statements))
|
|
150
|
+
|
|
151
|
+
loop = asyncio.get_event_loop()
|
|
152
|
+
await loop.run_in_executor(None, _send)
|
|
153
|
+
return user_result
|
|
154
|
+
|
|
155
|
+
def with_write_mode(self, mode: str) -> "AsyncClient":
|
|
156
|
+
c = AsyncClient.__new__(AsyncClient)
|
|
157
|
+
c._api_key = self._api_key
|
|
158
|
+
c._url = self._url
|
|
159
|
+
c._write_mode = mode
|
|
160
|
+
c._timeout = self._timeout
|
|
161
|
+
c._max_retries = self._max_retries
|
|
162
|
+
c._headers = {**self._headers, "X-Write-Mode": mode}
|
|
163
|
+
return c
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def strong(self) -> "AsyncClient":
|
|
167
|
+
c = AsyncClient.__new__(AsyncClient)
|
|
168
|
+
c._api_key = self._api_key
|
|
169
|
+
c._url = self._url
|
|
170
|
+
c._write_mode = self._write_mode
|
|
171
|
+
c._timeout = self._timeout
|
|
172
|
+
c._max_retries = self._max_retries
|
|
173
|
+
c._headers = {**self._headers, "X-Consistency": "strong"}
|
|
174
|
+
return c
|
|
175
|
+
|
|
176
|
+
def with_tag(self, tag: str) -> "AsyncClient":
|
|
177
|
+
c = AsyncClient.__new__(AsyncClient)
|
|
178
|
+
c._api_key = self._api_key
|
|
179
|
+
c._url = self._url
|
|
180
|
+
c._write_mode = self._write_mode
|
|
181
|
+
c._timeout = self._timeout
|
|
182
|
+
c._max_retries = self._max_retries
|
|
183
|
+
c._headers = {**self._headers, "X-Query-Tag": tag}
|
|
184
|
+
return c
|
|
185
|
+
|
|
186
|
+
def with_idempotency_key(self, key: str) -> "AsyncClient":
|
|
187
|
+
c = AsyncClient.__new__(AsyncClient)
|
|
188
|
+
c._api_key = self._api_key
|
|
189
|
+
c._url = self._url
|
|
190
|
+
c._write_mode = self._write_mode
|
|
191
|
+
c._timeout = self._timeout
|
|
192
|
+
c._max_retries = self._max_retries
|
|
193
|
+
c._headers = {**self._headers, "X-Idempotency-Key": key}
|
|
194
|
+
return c
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def async_connect(
|
|
198
|
+
api_key: Optional[str] = None,
|
|
199
|
+
endpoint: Optional[str] = None,
|
|
200
|
+
write_mode: str = "sync",
|
|
201
|
+
timeout: float = 30.0,
|
|
202
|
+
max_retries: int = 3,
|
|
203
|
+
tag: Optional[str] = None,
|
|
204
|
+
) -> AsyncClient:
|
|
205
|
+
"""
|
|
206
|
+
Create an async Deltex client.
|
|
207
|
+
|
|
208
|
+
Example:
|
|
209
|
+
async with deltex.async_connect() as db:
|
|
210
|
+
users = await db.query("SELECT * FROM users")
|
|
211
|
+
"""
|
|
212
|
+
return AsyncClient(
|
|
213
|
+
api_key=api_key, endpoint=endpoint, write_mode=write_mode,
|
|
214
|
+
timeout=timeout, max_retries=max_retries, tag=tag,
|
|
215
|
+
)
|