deltex 1.3.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.
- deltex/__init__.py +35 -0
- deltex/async_client.py +215 -0
- deltex/cli.py +686 -0
- deltex/client.py +382 -0
- deltex-1.3.1.dist-info/METADATA +207 -0
- deltex-1.3.1.dist-info/RECORD +8 -0
- deltex-1.3.1.dist-info/WHEEL +4 -0
- deltex-1.3.1.dist-info/entry_points.txt +2 -0
deltex/__init__.py
ADDED
|
@@ -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
|
+
]
|
deltex/async_client.py
ADDED
|
@@ -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
|
+
)
|