quillsql 2.2.6__tar.gz → 2.2.8__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.
- {quillsql-2.2.6 → quillsql-2.2.8}/PKG-INFO +42 -1
- quillsql-2.2.8/README.md +87 -0
- quillsql-2.2.8/quillsql/__init__.py +14 -0
- {quillsql-2.2.6 → quillsql-2.2.8}/quillsql/core.py +103 -22
- {quillsql-2.2.6 → quillsql-2.2.8}/quillsql/db/bigquery.py +2 -1
- {quillsql-2.2.6 → quillsql-2.2.8}/quillsql/db/cached_connection.py +32 -9
- {quillsql-2.2.6 → quillsql-2.2.8}/quillsql/utils/__init__.py +1 -0
- quillsql-2.2.8/quillsql/utils/post_quill_executor.py +69 -0
- {quillsql-2.2.6 → quillsql-2.2.8}/quillsql.egg-info/PKG-INFO +42 -1
- {quillsql-2.2.6 → quillsql-2.2.8}/quillsql.egg-info/SOURCES.txt +1 -0
- {quillsql-2.2.6 → quillsql-2.2.8}/setup.py +1 -1
- quillsql-2.2.6/README.md +0 -46
- quillsql-2.2.6/quillsql/__init__.py +0 -5
- {quillsql-2.2.6 → quillsql-2.2.8}/quillsql/assets/__init__.py +0 -0
- {quillsql-2.2.6 → quillsql-2.2.8}/quillsql/assets/pgtypes.py +0 -0
- {quillsql-2.2.6 → quillsql-2.2.8}/quillsql/db/__init__.py +0 -0
- {quillsql-2.2.6 → quillsql-2.2.8}/quillsql/db/db_helper.py +0 -0
- {quillsql-2.2.6 → quillsql-2.2.8}/quillsql/db/postgres.py +0 -0
- {quillsql-2.2.6 → quillsql-2.2.8}/quillsql/error.py +0 -0
- {quillsql-2.2.6 → quillsql-2.2.8}/quillsql/utils/filters.py +0 -0
- {quillsql-2.2.6 → quillsql-2.2.8}/quillsql/utils/pivot_template.py +0 -0
- {quillsql-2.2.6 → quillsql-2.2.8}/quillsql/utils/run_query_processes.py +0 -0
- {quillsql-2.2.6 → quillsql-2.2.8}/quillsql/utils/schema_conversion.py +0 -0
- {quillsql-2.2.6 → quillsql-2.2.8}/quillsql/utils/tenants.py +0 -0
- {quillsql-2.2.6 → quillsql-2.2.8}/quillsql.egg-info/dependency_links.txt +0 -0
- {quillsql-2.2.6 → quillsql-2.2.8}/quillsql.egg-info/requires.txt +0 -0
- {quillsql-2.2.6 → quillsql-2.2.8}/quillsql.egg-info/top_level.txt +0 -0
- {quillsql-2.2.6 → quillsql-2.2.8}/setup.cfg +0 -0
- {quillsql-2.2.6 → quillsql-2.2.8}/tests/test_core.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quillsql
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.8
|
|
4
4
|
Summary: Quill SDK for Python.
|
|
5
5
|
Home-page: https://github.com/quill-sql/quill-python
|
|
6
6
|
Author: Quill
|
|
@@ -67,3 +67,44 @@ async def quill_post(data: Request, user: dict = Depends(authenticate_jwt)):
|
|
|
67
67
|
|
|
68
68
|
Then you can run your app like normally. Pass in this route to our react library
|
|
69
69
|
on the frontend and you all set!
|
|
70
|
+
|
|
71
|
+
## Streaming
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from quillsql import Quill
|
|
75
|
+
from fastapi.responses import StreamingResponse
|
|
76
|
+
import asyncio
|
|
77
|
+
|
|
78
|
+
quill = Quill(
|
|
79
|
+
private_key=os.getenv("QULL_PRIVATE_KEY"),
|
|
80
|
+
database_connection_string=os.getenv("POSTGRES_READ"),
|
|
81
|
+
database_type="postgresql"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
@app.post("/quill-stream")
|
|
85
|
+
async def quill_post(data: Request, user: dict = Depends(authenticate_jwt)):
|
|
86
|
+
# assuming user fetched via auth middleware has an userId
|
|
87
|
+
user_id = user["user_id"]
|
|
88
|
+
body = await data.json()
|
|
89
|
+
metadata = body.get("metadata")
|
|
90
|
+
|
|
91
|
+
quill_stream = quill.stream(
|
|
92
|
+
tenants=[{"tenantField": "user_id", "tenantIds": [user_id]}],
|
|
93
|
+
metadata=metadata,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
async def event_generator():
|
|
97
|
+
# Full event types list: https://ai-sdk.dev/docs/ai-sdk-ui/stream-protocol#data-stream-protocol
|
|
98
|
+
async for event in quill_stream:
|
|
99
|
+
if event["type"] == "start":
|
|
100
|
+
pass
|
|
101
|
+
elif event["type"] == "text-delta":
|
|
102
|
+
yield event['delta']
|
|
103
|
+
elif event["type"] == "finish":
|
|
104
|
+
return
|
|
105
|
+
elif event["type"] == "error":
|
|
106
|
+
yield event['errorText']
|
|
107
|
+
await asyncio.sleep(0)
|
|
108
|
+
|
|
109
|
+
return StreamingResponse(event_generator(), media_type="text/event-stream")
|
|
110
|
+
```
|
quillsql-2.2.8/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Quill Python SDK
|
|
2
|
+
|
|
3
|
+
## Quickstart
|
|
4
|
+
|
|
5
|
+
First, install the quillsql package by running:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
$ pip install quillsql
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Then, add a `/quill` endpoint to your existing python server. For example, if
|
|
12
|
+
you were running a FASTAPI app, you would just add the endpoint like this:
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from quillsql import Quill
|
|
16
|
+
|
|
17
|
+
quill = Quill(
|
|
18
|
+
private_key=os.getenv("QULL_PRIVATE_KEY"),
|
|
19
|
+
database_connection_string=os.getenv("POSTGRES_READ"),
|
|
20
|
+
database_type="postgresql"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
security = HTTPBearer()
|
|
24
|
+
|
|
25
|
+
async def authenticate_jwt(token: str = Depends(security)):
|
|
26
|
+
# Your JWT validation logic here
|
|
27
|
+
# Return user object or raise HTTPException
|
|
28
|
+
user = validate_jwt_token(token.credentials)
|
|
29
|
+
return user
|
|
30
|
+
|
|
31
|
+
@app.post("/quill")
|
|
32
|
+
async def quill_post(data: Request, user: dict = Depends(authenticate_jwt)):
|
|
33
|
+
# assuming user fetched via auth middleware has an userId
|
|
34
|
+
user_id = user["user_id"]
|
|
35
|
+
body = await data.json()
|
|
36
|
+
metadata = body.get("metadata")
|
|
37
|
+
|
|
38
|
+
result = quill.query(
|
|
39
|
+
tenants=[{"tenantField": "user_id", "tenantIds": [user_id]}],
|
|
40
|
+
metadata=metadata
|
|
41
|
+
)
|
|
42
|
+
return result
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Then you can run your app like normally. Pass in this route to our react library
|
|
46
|
+
on the frontend and you all set!
|
|
47
|
+
|
|
48
|
+
## Streaming
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from quillsql import Quill
|
|
52
|
+
from fastapi.responses import StreamingResponse
|
|
53
|
+
import asyncio
|
|
54
|
+
|
|
55
|
+
quill = Quill(
|
|
56
|
+
private_key=os.getenv("QULL_PRIVATE_KEY"),
|
|
57
|
+
database_connection_string=os.getenv("POSTGRES_READ"),
|
|
58
|
+
database_type="postgresql"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
@app.post("/quill-stream")
|
|
62
|
+
async def quill_post(data: Request, user: dict = Depends(authenticate_jwt)):
|
|
63
|
+
# assuming user fetched via auth middleware has an userId
|
|
64
|
+
user_id = user["user_id"]
|
|
65
|
+
body = await data.json()
|
|
66
|
+
metadata = body.get("metadata")
|
|
67
|
+
|
|
68
|
+
quill_stream = quill.stream(
|
|
69
|
+
tenants=[{"tenantField": "user_id", "tenantIds": [user_id]}],
|
|
70
|
+
metadata=metadata,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
async def event_generator():
|
|
74
|
+
# Full event types list: https://ai-sdk.dev/docs/ai-sdk-ui/stream-protocol#data-stream-protocol
|
|
75
|
+
async for event in quill_stream:
|
|
76
|
+
if event["type"] == "start":
|
|
77
|
+
pass
|
|
78
|
+
elif event["type"] == "text-delta":
|
|
79
|
+
yield event['delta']
|
|
80
|
+
elif event["type"] == "finish":
|
|
81
|
+
return
|
|
82
|
+
elif event["type"] == "error":
|
|
83
|
+
yield event['errorText']
|
|
84
|
+
await asyncio.sleep(0)
|
|
85
|
+
|
|
86
|
+
return StreamingResponse(event_generator(), media_type="text/event-stream")
|
|
87
|
+
```
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import codecs
|
|
2
3
|
from dotenv import load_dotenv
|
|
3
4
|
|
|
4
5
|
import requests
|
|
@@ -29,7 +30,7 @@ load_dotenv()
|
|
|
29
30
|
|
|
30
31
|
ENV = os.getenv("PYTHON_ENV")
|
|
31
32
|
DEV_HOST = "http://localhost:8080"
|
|
32
|
-
PROD_HOST = "https://quill
|
|
33
|
+
PROD_HOST = "https://api.quill.co"
|
|
33
34
|
HOST = DEV_HOST if ENV == "development" else PROD_HOST
|
|
34
35
|
|
|
35
36
|
SINGLE_TENANT = "QUILL_SINGLE_TENANT"
|
|
@@ -144,10 +145,14 @@ class Quill:
|
|
|
144
145
|
None
|
|
145
146
|
)
|
|
146
147
|
|
|
147
|
-
|
|
148
|
-
distinct_value_results
|
|
149
|
-
|
|
150
|
-
|
|
148
|
+
distinct_value_query_results = (
|
|
149
|
+
distinct_value_results or {}
|
|
150
|
+
).get("queryResults") or []
|
|
151
|
+
if distinct_value_query_results:
|
|
152
|
+
distinct_values = parse_distinct_values(
|
|
153
|
+
distinct_value_query_results[0],
|
|
154
|
+
config.get("databaseType")
|
|
155
|
+
)
|
|
151
156
|
|
|
152
157
|
try:
|
|
153
158
|
final_query = hydrate_pivot_template(template, distinct_values, config)
|
|
@@ -261,17 +266,16 @@ class Quill:
|
|
|
261
266
|
else:
|
|
262
267
|
tenant_flags = flags
|
|
263
268
|
|
|
264
|
-
|
|
265
|
-
self.run_queries(
|
|
269
|
+
if metadata.get("preQueries"):
|
|
270
|
+
pre_query_results = self.run_queries(
|
|
266
271
|
metadata.get("preQueries"),
|
|
267
272
|
self.target_connection.database_type,
|
|
268
273
|
metadata.get("databaseType"),
|
|
269
274
|
metadata,
|
|
270
275
|
metadata.get("runQueryConfig"),
|
|
271
276
|
)
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
)
|
|
277
|
+
else:
|
|
278
|
+
pre_query_results = {}
|
|
275
279
|
|
|
276
280
|
if metadata.get("runQueryConfig") and metadata.get("runQueryConfig").get(
|
|
277
281
|
"overridePost"
|
|
@@ -383,6 +387,80 @@ class Quill:
|
|
|
383
387
|
"status": "error",
|
|
384
388
|
"data": responseMetadata,
|
|
385
389
|
}
|
|
390
|
+
|
|
391
|
+
async def stream(
|
|
392
|
+
self,
|
|
393
|
+
tenants,
|
|
394
|
+
metadata,
|
|
395
|
+
flags=None,
|
|
396
|
+
):
|
|
397
|
+
if not tenants:
|
|
398
|
+
raise ValueError("You may not pass an empty tenants array.")
|
|
399
|
+
|
|
400
|
+
if not metadata:
|
|
401
|
+
yield {"type": "error", "errorText": "Missing metadata."}
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
task = metadata.get("task")
|
|
405
|
+
if not task:
|
|
406
|
+
yield {"type": "error", "errorText": "Missing task."}
|
|
407
|
+
return
|
|
408
|
+
|
|
409
|
+
try:
|
|
410
|
+
# Set tenant IDs in the connection
|
|
411
|
+
self.target_connection.tenant_ids = extract_tenant_ids(tenants)
|
|
412
|
+
|
|
413
|
+
# Handle tenant flags synthesis
|
|
414
|
+
tenant_flags = None
|
|
415
|
+
if tenants[0] == SINGLE_TENANT and flags:
|
|
416
|
+
if flags and isinstance(flags[0], dict):
|
|
417
|
+
tenant_flags = [{'tenantField': SINGLE_TENANT, 'flags': flags}]
|
|
418
|
+
else:
|
|
419
|
+
tenant_flags = flags
|
|
420
|
+
|
|
421
|
+
payload = {
|
|
422
|
+
**metadata,
|
|
423
|
+
"tenants": tenants,
|
|
424
|
+
"flags": tenant_flags,
|
|
425
|
+
}
|
|
426
|
+
# Custom JSON Encoder to handle Enums
|
|
427
|
+
class EnumEncoder(json.JSONEncoder):
|
|
428
|
+
def default(self, obj):
|
|
429
|
+
if isinstance(obj, Enum):
|
|
430
|
+
return obj.value # Convert enum to its value (string in this case)
|
|
431
|
+
return super().default(obj)
|
|
432
|
+
url = f"{self.baseUrl}/sdk/{task}"
|
|
433
|
+
headers = {"Authorization": f"Bearer {self.private_key}", "Content-Type": "application/json","Accept": "text/event-stream"}
|
|
434
|
+
encoded = json.dumps(payload, cls=EnumEncoder)
|
|
435
|
+
|
|
436
|
+
resp = requests.post(url, data=encoded, headers=headers, stream=True)
|
|
437
|
+
decoder = codecs.getincrementaldecoder('utf-8')()
|
|
438
|
+
buf = ""
|
|
439
|
+
for chunk in resp.iter_content(chunk_size=4096):
|
|
440
|
+
buf += decoder.decode(chunk)
|
|
441
|
+
while "\n\n" in buf:
|
|
442
|
+
raw_event, buf = buf.split("\n\n", 1)
|
|
443
|
+
data_lines = []
|
|
444
|
+
for line in raw_event.splitlines():
|
|
445
|
+
if line.startswith("data:"):
|
|
446
|
+
data_lines.append(line[len("data:"):].strip())
|
|
447
|
+
if not data_lines:
|
|
448
|
+
continue
|
|
449
|
+
payload = "\n".join(data_lines)
|
|
450
|
+
if payload == "[DONE]":
|
|
451
|
+
break
|
|
452
|
+
yield json.loads(payload)
|
|
453
|
+
|
|
454
|
+
# flush any partial code points at the end
|
|
455
|
+
buf += decoder.decode(b"", final=True)
|
|
456
|
+
yield buf
|
|
457
|
+
return
|
|
458
|
+
except Exception as err:
|
|
459
|
+
yield {
|
|
460
|
+
"type": "error",
|
|
461
|
+
"errorText": str(err).splitlines()[0],
|
|
462
|
+
}
|
|
463
|
+
return
|
|
386
464
|
|
|
387
465
|
def apply_limit(self, query, limit):
|
|
388
466
|
# Simple logic: if query already has a limit, don't add another
|
|
@@ -396,24 +474,27 @@ class Quill:
|
|
|
396
474
|
return query
|
|
397
475
|
return f"{query.rstrip(';')} limit {limit}"
|
|
398
476
|
|
|
399
|
-
def normalize_database_type(self, db_type):
|
|
400
|
-
if not db_type:
|
|
401
|
-
return None
|
|
402
|
-
lowered = db_type.lower()
|
|
403
|
-
if lowered in ("postgresql", "postgres"):
|
|
404
|
-
return "postgres"
|
|
405
|
-
return lowered
|
|
406
|
-
|
|
407
477
|
def run_queries(
|
|
408
478
|
self, queries, pkDatabaseType, databaseType=None, metadata=None, runQueryConfig=None
|
|
409
479
|
):
|
|
410
480
|
results = {}
|
|
411
481
|
if not queries:
|
|
412
482
|
return {"queryResults": []}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
483
|
+
def _normalize_db_type(db_type):
|
|
484
|
+
if not db_type:
|
|
485
|
+
return None
|
|
486
|
+
lowered = db_type.lower()
|
|
487
|
+
if lowered in ("postgresql", "postgres"):
|
|
488
|
+
return "postgres"
|
|
489
|
+
return lowered
|
|
490
|
+
|
|
491
|
+
normalized_metadata_db = _normalize_db_type(databaseType)
|
|
492
|
+
normalized_backend_db = _normalize_db_type(pkDatabaseType)
|
|
493
|
+
if (
|
|
494
|
+
normalized_metadata_db
|
|
495
|
+
and normalized_backend_db
|
|
496
|
+
and normalized_metadata_db != normalized_backend_db
|
|
497
|
+
):
|
|
417
498
|
return {"dbMismatched": True, "backendDatabaseType": pkDatabaseType}
|
|
418
499
|
if runQueryConfig and runQueryConfig.get("arrayToMap"):
|
|
419
500
|
mapped_array = array_to_map(
|
|
@@ -26,7 +26,8 @@ def format_bigquery_config(connection_string):
|
|
|
26
26
|
"project": service_account.get("project_id"),
|
|
27
27
|
"credentials": service_account,
|
|
28
28
|
}
|
|
29
|
-
except (ValueError, TypeError):
|
|
29
|
+
except (ValueError, TypeError) as e:
|
|
30
|
+
print("Invalid service account JSON.", e)
|
|
30
31
|
return connection_string
|
|
31
32
|
|
|
32
33
|
|
|
@@ -29,20 +29,43 @@ class CachedConnection:
|
|
|
29
29
|
)
|
|
30
30
|
return None
|
|
31
31
|
|
|
32
|
+
def _is_connection_closed(self):
|
|
33
|
+
return self.connection is None or getattr(self.connection, "closed", True)
|
|
34
|
+
|
|
35
|
+
def _open_connection(self):
|
|
36
|
+
self.connection = connect_to_db(
|
|
37
|
+
self.database_type, self.config, self.using_connection_string
|
|
38
|
+
)
|
|
39
|
+
return self.connection
|
|
40
|
+
|
|
41
|
+
def _close_connection(self):
|
|
42
|
+
if self.connection is None:
|
|
43
|
+
return
|
|
44
|
+
try:
|
|
45
|
+
self.connection.close()
|
|
46
|
+
except Exception:
|
|
47
|
+
pass
|
|
48
|
+
finally:
|
|
49
|
+
self.connection = None
|
|
50
|
+
|
|
32
51
|
def exec_with_reconnect(self, sql):
|
|
33
52
|
reconnect_count = 0
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
self.database_type, self.config, self.using_connection_string
|
|
37
|
-
)
|
|
53
|
+
last_error = None
|
|
54
|
+
while reconnect_count < 10:
|
|
38
55
|
try:
|
|
56
|
+
if self._is_connection_closed():
|
|
57
|
+
self._open_connection()
|
|
39
58
|
return run_query_by_db(self.database_type, sql, self.connection)
|
|
40
59
|
except psycopg2.Error as err:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
60
|
+
last_error = err
|
|
61
|
+
reconnect_count += 1
|
|
62
|
+
self._close_connection()
|
|
63
|
+
except Exception:
|
|
64
|
+
self._close_connection()
|
|
65
|
+
raise
|
|
66
|
+
diag = getattr(last_error, "diag", None)
|
|
67
|
+
position = getattr(diag, "statement_position", None)
|
|
68
|
+
raise PgQueryError(last_error or "Failed to execute query", sql, position)
|
|
46
69
|
|
|
47
70
|
def exec(self, sql):
|
|
48
71
|
try:
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
from concurrent.futures import Future, ThreadPoolExecutor
|
|
5
|
+
from typing import Callable, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ParallelPostQuill:
|
|
9
|
+
"""
|
|
10
|
+
Provides a thread-pooled, drop-in replacement for Quill.post_quill.
|
|
11
|
+
|
|
12
|
+
Example
|
|
13
|
+
-------
|
|
14
|
+
parallel_post = ParallelPostQuill(
|
|
15
|
+
quill_factory=lambda: Quill(...),
|
|
16
|
+
max_workers=4,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# Synchronous (behaves like the original post_quill)
|
|
20
|
+
response = parallel_post("report", payload)
|
|
21
|
+
|
|
22
|
+
# Async-style execution
|
|
23
|
+
future = parallel_post("report", payload, wait=False)
|
|
24
|
+
response = future.result()
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
quill_factory: Callable[[], "Quill"],
|
|
30
|
+
max_workers: int = 5,
|
|
31
|
+
executor: Optional[ThreadPoolExecutor] = None,
|
|
32
|
+
):
|
|
33
|
+
if executor is None:
|
|
34
|
+
self._executor = ThreadPoolExecutor(
|
|
35
|
+
max_workers=max_workers,
|
|
36
|
+
thread_name_prefix="post-quill",
|
|
37
|
+
)
|
|
38
|
+
self._owns_executor = True
|
|
39
|
+
else:
|
|
40
|
+
self._executor = executor
|
|
41
|
+
self._owns_executor = False
|
|
42
|
+
|
|
43
|
+
if not callable(quill_factory):
|
|
44
|
+
raise ValueError("quill_factory must be callable and create Quill instances.")
|
|
45
|
+
|
|
46
|
+
self._quill_factory = quill_factory
|
|
47
|
+
self._thread_local = threading.local()
|
|
48
|
+
|
|
49
|
+
def __call__(self, path: str, payload: dict, wait: bool = True):
|
|
50
|
+
future: Future = self._executor.submit(self._invoke, path, payload)
|
|
51
|
+
if wait:
|
|
52
|
+
return future.result()
|
|
53
|
+
return future
|
|
54
|
+
|
|
55
|
+
def shutdown(self, wait: bool = True):
|
|
56
|
+
if self._owns_executor:
|
|
57
|
+
self._executor.shutdown(wait=wait)
|
|
58
|
+
|
|
59
|
+
def _get_quill(self):
|
|
60
|
+
quill = getattr(self._thread_local, "quill", None)
|
|
61
|
+
if quill is None:
|
|
62
|
+
quill = self._quill_factory()
|
|
63
|
+
self._thread_local.quill = quill
|
|
64
|
+
return quill
|
|
65
|
+
|
|
66
|
+
def _invoke(self, path: str, payload: dict):
|
|
67
|
+
quill = self._get_quill()
|
|
68
|
+
return quill.post_quill(path, payload)
|
|
69
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quillsql
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.8
|
|
4
4
|
Summary: Quill SDK for Python.
|
|
5
5
|
Home-page: https://github.com/quill-sql/quill-python
|
|
6
6
|
Author: Quill
|
|
@@ -67,3 +67,44 @@ async def quill_post(data: Request, user: dict = Depends(authenticate_jwt)):
|
|
|
67
67
|
|
|
68
68
|
Then you can run your app like normally. Pass in this route to our react library
|
|
69
69
|
on the frontend and you all set!
|
|
70
|
+
|
|
71
|
+
## Streaming
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from quillsql import Quill
|
|
75
|
+
from fastapi.responses import StreamingResponse
|
|
76
|
+
import asyncio
|
|
77
|
+
|
|
78
|
+
quill = Quill(
|
|
79
|
+
private_key=os.getenv("QULL_PRIVATE_KEY"),
|
|
80
|
+
database_connection_string=os.getenv("POSTGRES_READ"),
|
|
81
|
+
database_type="postgresql"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
@app.post("/quill-stream")
|
|
85
|
+
async def quill_post(data: Request, user: dict = Depends(authenticate_jwt)):
|
|
86
|
+
# assuming user fetched via auth middleware has an userId
|
|
87
|
+
user_id = user["user_id"]
|
|
88
|
+
body = await data.json()
|
|
89
|
+
metadata = body.get("metadata")
|
|
90
|
+
|
|
91
|
+
quill_stream = quill.stream(
|
|
92
|
+
tenants=[{"tenantField": "user_id", "tenantIds": [user_id]}],
|
|
93
|
+
metadata=metadata,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
async def event_generator():
|
|
97
|
+
# Full event types list: https://ai-sdk.dev/docs/ai-sdk-ui/stream-protocol#data-stream-protocol
|
|
98
|
+
async for event in quill_stream:
|
|
99
|
+
if event["type"] == "start":
|
|
100
|
+
pass
|
|
101
|
+
elif event["type"] == "text-delta":
|
|
102
|
+
yield event['delta']
|
|
103
|
+
elif event["type"] == "finish":
|
|
104
|
+
return
|
|
105
|
+
elif event["type"] == "error":
|
|
106
|
+
yield event['errorText']
|
|
107
|
+
await asyncio.sleep(0)
|
|
108
|
+
|
|
109
|
+
return StreamingResponse(event_generator(), media_type="text/event-stream")
|
|
110
|
+
```
|
|
@@ -18,6 +18,7 @@ quillsql/db/postgres.py
|
|
|
18
18
|
quillsql/utils/__init__.py
|
|
19
19
|
quillsql/utils/filters.py
|
|
20
20
|
quillsql/utils/pivot_template.py
|
|
21
|
+
quillsql/utils/post_quill_executor.py
|
|
21
22
|
quillsql/utils/run_query_processes.py
|
|
22
23
|
quillsql/utils/schema_conversion.py
|
|
23
24
|
quillsql/utils/tenants.py
|
quillsql-2.2.6/README.md
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
# Quill Python SDK
|
|
2
|
-
|
|
3
|
-
## Quickstart
|
|
4
|
-
|
|
5
|
-
First, install the quillsql package by running:
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
$ pip install quillsql
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
Then, add a `/quill` endpoint to your existing python server. For example, if
|
|
12
|
-
you were running a FASTAPI app, you would just add the endpoint like this:
|
|
13
|
-
|
|
14
|
-
```python
|
|
15
|
-
from quillsql import Quill
|
|
16
|
-
|
|
17
|
-
quill = Quill(
|
|
18
|
-
private_key=os.getenv("QULL_PRIVATE_KEY"),
|
|
19
|
-
database_connection_string=os.getenv("POSTGRES_READ"),
|
|
20
|
-
database_type="postgresql"
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
security = HTTPBearer()
|
|
24
|
-
|
|
25
|
-
async def authenticate_jwt(token: str = Depends(security)):
|
|
26
|
-
# Your JWT validation logic here
|
|
27
|
-
# Return user object or raise HTTPException
|
|
28
|
-
user = validate_jwt_token(token.credentials)
|
|
29
|
-
return user
|
|
30
|
-
|
|
31
|
-
@app.post("/quill")
|
|
32
|
-
async def quill_post(data: Request, user: dict = Depends(authenticate_jwt)):
|
|
33
|
-
# assuming user fetched via auth middleware has an userId
|
|
34
|
-
user_id = user["user_id"]
|
|
35
|
-
body = await data.json()
|
|
36
|
-
metadata = body.get("metadata")
|
|
37
|
-
|
|
38
|
-
result = quill.query(
|
|
39
|
-
tenants=[{"tenantField": "user_id", "tenantIds": [user_id]}],
|
|
40
|
-
metadata=metadata
|
|
41
|
-
)
|
|
42
|
-
return result
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
Then you can run your app like normally. Pass in this route to our react library
|
|
46
|
-
on the frontend and you all set!
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|