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 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
+ )