fleet-python 0.2.98__tar.gz → 0.2.100__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.
- {fleet_python-0.2.98/fleet_python.egg-info → fleet_python-0.2.100}/PKG-INFO +1 -1
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/__init__.py +1 -1
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/__init__.py +1 -1
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/base.py +1 -1
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/resources/sqlite.py +156 -30
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/base.py +1 -1
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/resources/sqlite.py +116 -1
- {fleet_python-0.2.98 → fleet_python-0.2.100/fleet_python.egg-info}/PKG-INFO +1 -1
- {fleet_python-0.2.98 → fleet_python-0.2.100}/pyproject.toml +1 -1
- {fleet_python-0.2.98 → fleet_python-0.2.100}/LICENSE +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/README.md +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/diff_example.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/dsl_example.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/example.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/exampleResume.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/example_account.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/example_action_log.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/example_client.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/example_mcp_anthropic.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/example_mcp_openai.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/example_sync.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/example_task.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/example_tasks.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/example_verifier.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/export_tasks.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/fetch_tasks.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/gemini_example.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/import_tasks.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/iterate_verifiers.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/json_tasks_example.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/nova_act_example.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/openai_example.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/openai_simple_example.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/query_builder_example.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/quickstart.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/test_cdp_logging.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/client.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/env/__init__.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/env/client.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/exceptions.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/global_client.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/instance/__init__.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/instance/base.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/instance/client.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/models.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/resources/__init__.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/resources/api.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/resources/base.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/resources/browser.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/resources/mcp.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/tasks.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/verifiers/__init__.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/verifiers/bundler.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/verifiers/verifier.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/agent/__init__.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/agent/gemini_cua/Dockerfile +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/agent/gemini_cua/__init__.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/agent/gemini_cua/agent.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/agent/gemini_cua/mcp/main.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/agent/gemini_cua/mcp_server/__init__.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/agent/gemini_cua/mcp_server/main.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/agent/gemini_cua/mcp_server/tools.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/agent/gemini_cua/requirements.txt +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/agent/gemini_cua/start.sh +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/agent/orchestrator.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/agent/types.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/agent/utils.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/cli.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/client.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/config.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/env/__init__.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/env/client.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/eval/__init__.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/eval/uploader.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/exceptions.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/global_client.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/instance/__init__.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/instance/base.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/instance/client.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/instance/models.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/models.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/proxy/__init__.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/proxy/proxy.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/proxy/whitelist.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/resources/__init__.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/resources/api.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/resources/base.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/resources/browser.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/resources/mcp.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/tasks.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/types.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/utils/__init__.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/utils/http_logging.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/utils/logging.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/utils/playwright.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/verifiers/__init__.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/verifiers/bundler.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/verifiers/code.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/verifiers/db.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/verifiers/decorator.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/verifiers/parse.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/verifiers/sql_differ.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/verifiers/verifier.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet_python.egg-info/SOURCES.txt +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet_python.egg-info/dependency_links.txt +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet_python.egg-info/entry_points.txt +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet_python.egg-info/requires.txt +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet_python.egg-info/top_level.txt +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/scripts/fix_sync_imports.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/scripts/unasync.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/setup.cfg +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/tests/__init__.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/tests/test_app_method.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/tests/test_expect_only.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/tests/test_instance_dispatch.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/tests/test_sqlite_resource_dual_mode.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/tests/test_sqlite_shared_memory_behavior.py +0 -0
- {fleet_python-0.2.98 → fleet_python-0.2.100}/tests/test_verifier_from_string.py +0 -0
|
@@ -7,6 +7,8 @@ import tempfile
|
|
|
7
7
|
import sqlite3
|
|
8
8
|
import os
|
|
9
9
|
import asyncio
|
|
10
|
+
import re
|
|
11
|
+
import json
|
|
10
12
|
|
|
11
13
|
from typing import TYPE_CHECKING
|
|
12
14
|
|
|
@@ -23,6 +25,17 @@ from fleet.verifiers.db import (
|
|
|
23
25
|
)
|
|
24
26
|
|
|
25
27
|
|
|
28
|
+
def _quote_identifier(identifier: str) -> str:
|
|
29
|
+
"""Quote an identifier (table or column name) for SQLite.
|
|
30
|
+
|
|
31
|
+
SQLite uses double quotes for identifiers and escapes internal quotes by doubling them.
|
|
32
|
+
This handles reserved keywords like 'order', 'table', etc.
|
|
33
|
+
"""
|
|
34
|
+
# Escape any double quotes in the identifier by doubling them
|
|
35
|
+
escaped = identifier.replace('"', '""')
|
|
36
|
+
return f'"{escaped}"'
|
|
37
|
+
|
|
38
|
+
|
|
26
39
|
class AsyncDatabaseSnapshot:
|
|
27
40
|
"""Lazy database snapshot that fetches data on-demand through API."""
|
|
28
41
|
|
|
@@ -57,12 +70,12 @@ class AsyncDatabaseSnapshot:
|
|
|
57
70
|
return
|
|
58
71
|
|
|
59
72
|
# Get table schema
|
|
60
|
-
schema_response = await self.resource.query(f"PRAGMA table_info({table})")
|
|
73
|
+
schema_response = await self.resource.query(f"PRAGMA table_info({_quote_identifier(table)})")
|
|
61
74
|
if schema_response.rows:
|
|
62
75
|
self._schemas[table] = [row[1] for row in schema_response.rows] # Column names
|
|
63
76
|
|
|
64
77
|
# Get all data for this table
|
|
65
|
-
data_response = await self.resource.query(f"SELECT * FROM {table}")
|
|
78
|
+
data_response = await self.resource.query(f"SELECT * FROM {_quote_identifier(table)}")
|
|
66
79
|
if data_response.rows and data_response.columns:
|
|
67
80
|
self._data[table] = [
|
|
68
81
|
dict(zip(data_response.columns, row)) for row in data_response.rows
|
|
@@ -123,23 +136,23 @@ class AsyncSnapshotQueryBuilder:
|
|
|
123
136
|
where_parts = []
|
|
124
137
|
for col, op, val in self._conditions:
|
|
125
138
|
if op == "=" and val is None:
|
|
126
|
-
where_parts.append(f"{col} IS NULL")
|
|
139
|
+
where_parts.append(f"{_quote_identifier(col)} IS NULL")
|
|
127
140
|
elif op == "IS":
|
|
128
|
-
where_parts.append(f"{col} IS NULL")
|
|
141
|
+
where_parts.append(f"{_quote_identifier(col)} IS NULL")
|
|
129
142
|
elif op == "IS NOT":
|
|
130
|
-
where_parts.append(f"{col} IS NOT NULL")
|
|
143
|
+
where_parts.append(f"{_quote_identifier(col)} IS NOT NULL")
|
|
131
144
|
elif op == "=":
|
|
132
145
|
if isinstance(val, str):
|
|
133
146
|
escaped_val = val.replace("'", "''")
|
|
134
|
-
where_parts.append(f"{col} = '{escaped_val}'")
|
|
147
|
+
where_parts.append(f"{_quote_identifier(col)} = '{escaped_val}'")
|
|
135
148
|
else:
|
|
136
|
-
where_parts.append(f"{col} = '{val}'")
|
|
149
|
+
where_parts.append(f"{_quote_identifier(col)} = '{val}'")
|
|
137
150
|
|
|
138
151
|
where_clause = " AND ".join(where_parts)
|
|
139
152
|
|
|
140
153
|
# Build full query
|
|
141
154
|
cols = ", ".join(self._select_cols)
|
|
142
|
-
query = f"SELECT {cols} FROM {self._table} WHERE {where_clause}"
|
|
155
|
+
query = f"SELECT {cols} FROM {_quote_identifier(self._table)} WHERE {where_clause}"
|
|
143
156
|
|
|
144
157
|
if self._order_by:
|
|
145
158
|
query += f" ORDER BY {self._order_by}"
|
|
@@ -271,7 +284,7 @@ class AsyncSnapshotDiff:
|
|
|
271
284
|
async def _get_primary_key_columns(self, table: str) -> List[str]:
|
|
272
285
|
"""Get primary key columns for a table."""
|
|
273
286
|
# Try to get from schema
|
|
274
|
-
schema_response = await self.after.resource.query(f"PRAGMA table_info({table})")
|
|
287
|
+
schema_response = await self.after.resource.query(f"PRAGMA table_info({_quote_identifier(table)})")
|
|
275
288
|
if not schema_response.rows:
|
|
276
289
|
return ["id"] # Default fallback
|
|
277
290
|
|
|
@@ -414,18 +427,18 @@ class AsyncSnapshotDiff:
|
|
|
414
427
|
return f"'{val}'"
|
|
415
428
|
|
|
416
429
|
if len(pk_columns) == 1:
|
|
417
|
-
return f"{pk_columns[0]} = {escape_value(pk_value)}"
|
|
430
|
+
return f"{_quote_identifier(pk_columns[0])} = {escape_value(pk_value)}"
|
|
418
431
|
else:
|
|
419
432
|
# Composite key
|
|
420
433
|
if isinstance(pk_value, tuple):
|
|
421
434
|
conditions = [
|
|
422
|
-
f"{col} = {escape_value(val)}"
|
|
435
|
+
f"{_quote_identifier(col)} = {escape_value(val)}"
|
|
423
436
|
for col, val in zip(pk_columns, pk_value)
|
|
424
437
|
]
|
|
425
438
|
return " AND ".join(conditions)
|
|
426
439
|
else:
|
|
427
440
|
# Shouldn't happen if data is consistent
|
|
428
|
-
return f"{pk_columns[0]} = {escape_value(pk_value)}"
|
|
441
|
+
return f"{_quote_identifier(pk_columns[0])} = {escape_value(pk_value)}"
|
|
429
442
|
|
|
430
443
|
async def _expect_no_changes(self):
|
|
431
444
|
"""Efficiently verify that no changes occurred between snapshots using row counts."""
|
|
@@ -472,7 +485,7 @@ class AsyncSnapshotDiff:
|
|
|
472
485
|
|
|
473
486
|
if table in before_tables:
|
|
474
487
|
before_count_response = await self.before.resource.query(
|
|
475
|
-
f"SELECT COUNT(*) FROM {table}"
|
|
488
|
+
f"SELECT COUNT(*) FROM {_quote_identifier(table)}"
|
|
476
489
|
)
|
|
477
490
|
before_count = (
|
|
478
491
|
before_count_response.rows[0][0]
|
|
@@ -482,7 +495,7 @@ class AsyncSnapshotDiff:
|
|
|
482
495
|
|
|
483
496
|
if table in after_tables:
|
|
484
497
|
after_count_response = await self.after.resource.query(
|
|
485
|
-
f"SELECT COUNT(*) FROM {table}"
|
|
498
|
+
f"SELECT COUNT(*) FROM {_quote_identifier(table)}"
|
|
486
499
|
)
|
|
487
500
|
after_count = (
|
|
488
501
|
after_count_response.rows[0][0]
|
|
@@ -549,10 +562,10 @@ class AsyncSnapshotDiff:
|
|
|
549
562
|
order_by = ", ".join(pk_columns) if pk_columns else "rowid"
|
|
550
563
|
|
|
551
564
|
before_response = await self.before.resource.query(
|
|
552
|
-
f"SELECT * FROM {table} ORDER BY {order_by}"
|
|
565
|
+
f"SELECT * FROM {_quote_identifier(table)} ORDER BY {order_by}"
|
|
553
566
|
)
|
|
554
567
|
after_response = await self.after.resource.query(
|
|
555
|
-
f"SELECT * FROM {table} ORDER BY {order_by}"
|
|
568
|
+
f"SELECT * FROM {_quote_identifier(table)} ORDER BY {order_by}"
|
|
556
569
|
)
|
|
557
570
|
|
|
558
571
|
# Quick check: if column counts differ, there's a schema change
|
|
@@ -634,7 +647,7 @@ class AsyncSnapshotDiff:
|
|
|
634
647
|
where_sql = self._build_pk_where_clause(pk_columns, pk)
|
|
635
648
|
|
|
636
649
|
# Query before snapshot
|
|
637
|
-
before_query = f"SELECT * FROM {table} WHERE {where_sql}"
|
|
650
|
+
before_query = f"SELECT * FROM {_quote_identifier(table)} WHERE {where_sql}"
|
|
638
651
|
before_response = await self.before.resource.query(before_query)
|
|
639
652
|
before_row = (
|
|
640
653
|
dict(zip(before_response.columns, before_response.rows[0]))
|
|
@@ -727,7 +740,7 @@ class AsyncSnapshotDiff:
|
|
|
727
740
|
try:
|
|
728
741
|
# For tables with no allowed changes, just check row counts
|
|
729
742
|
before_count_response = await self.before.resource.query(
|
|
730
|
-
f"SELECT COUNT(*) FROM {table}"
|
|
743
|
+
f"SELECT COUNT(*) FROM {_quote_identifier(table)}"
|
|
731
744
|
)
|
|
732
745
|
before_count = (
|
|
733
746
|
before_count_response.rows[0][0]
|
|
@@ -736,7 +749,7 @@ class AsyncSnapshotDiff:
|
|
|
736
749
|
)
|
|
737
750
|
|
|
738
751
|
after_count_response = await self.after.resource.query(
|
|
739
|
-
f"SELECT COUNT(*) FROM {table}"
|
|
752
|
+
f"SELECT COUNT(*) FROM {_quote_identifier(table)}"
|
|
740
753
|
)
|
|
741
754
|
after_count = (
|
|
742
755
|
after_count_response.rows[0][0] if after_count_response.rows else 0
|
|
@@ -1098,7 +1111,7 @@ class AsyncSnapshotDiff:
|
|
|
1098
1111
|
where_sql = self._build_pk_where_clause(pk_columns, pk)
|
|
1099
1112
|
|
|
1100
1113
|
# Query before snapshot
|
|
1101
|
-
before_query = f"SELECT * FROM {table} WHERE {where_sql}"
|
|
1114
|
+
before_query = f"SELECT * FROM {_quote_identifier(table)} WHERE {where_sql}"
|
|
1102
1115
|
before_response = await self.before.resource.query(before_query)
|
|
1103
1116
|
before_row = (
|
|
1104
1117
|
dict(zip(before_response.columns, before_response.rows[0]))
|
|
@@ -1176,7 +1189,7 @@ class AsyncSnapshotDiff:
|
|
|
1176
1189
|
try:
|
|
1177
1190
|
# For tables with no allowed changes, just check row counts
|
|
1178
1191
|
before_count_response = await self.before.resource.query(
|
|
1179
|
-
f"SELECT COUNT(*) FROM {table}"
|
|
1192
|
+
f"SELECT COUNT(*) FROM {_quote_identifier(table)}"
|
|
1180
1193
|
)
|
|
1181
1194
|
before_count = (
|
|
1182
1195
|
before_count_response.rows[0][0]
|
|
@@ -1185,7 +1198,7 @@ class AsyncSnapshotDiff:
|
|
|
1185
1198
|
)
|
|
1186
1199
|
|
|
1187
1200
|
after_count_response = await self.after.resource.query(
|
|
1188
|
-
f"SELECT COUNT(*) FROM {table}"
|
|
1201
|
+
f"SELECT COUNT(*) FROM {_quote_identifier(table)}"
|
|
1189
1202
|
)
|
|
1190
1203
|
after_count = (
|
|
1191
1204
|
after_count_response.rows[0][0] if after_count_response.rows else 0
|
|
@@ -1950,13 +1963,13 @@ class AsyncQueryBuilder:
|
|
|
1950
1963
|
# Compile to SQL
|
|
1951
1964
|
def _compile(self) -> Tuple[str, List[Any]]:
|
|
1952
1965
|
cols = ", ".join(self._select_cols)
|
|
1953
|
-
sql = [f"SELECT {cols} FROM {self._table}"]
|
|
1966
|
+
sql = [f"SELECT {cols} FROM {_quote_identifier(self._table)}"]
|
|
1954
1967
|
params: List[Any] = []
|
|
1955
1968
|
|
|
1956
1969
|
# Joins
|
|
1957
1970
|
for tbl, onmap in self._joins:
|
|
1958
|
-
join_clauses = [f"{self._table}.{l} = {tbl}.{r}" for l, r in onmap.items()]
|
|
1959
|
-
sql.append(f"JOIN {tbl} ON {' AND '.join(join_clauses)}")
|
|
1971
|
+
join_clauses = [f"{_quote_identifier(self._table)}.{_quote_identifier(l)} = {_quote_identifier(tbl)}.{_quote_identifier(r)}" for l, r in onmap.items()]
|
|
1972
|
+
sql.append(f"JOIN {_quote_identifier(tbl)} ON {' AND '.join(join_clauses)}")
|
|
1960
1973
|
|
|
1961
1974
|
# WHERE
|
|
1962
1975
|
if self._conditions:
|
|
@@ -1964,12 +1977,12 @@ class AsyncQueryBuilder:
|
|
|
1964
1977
|
for col, op, val in self._conditions:
|
|
1965
1978
|
if op in ("IN", "NOT IN") and isinstance(val, tuple):
|
|
1966
1979
|
ph = ", ".join(["?" for _ in val])
|
|
1967
|
-
placeholders.append(f"{col} {op} ({ph})")
|
|
1980
|
+
placeholders.append(f"{_quote_identifier(col)} {op} ({ph})")
|
|
1968
1981
|
params.extend(val)
|
|
1969
1982
|
elif op in ("IS", "IS NOT"):
|
|
1970
|
-
placeholders.append(f"{col} {op} NULL")
|
|
1983
|
+
placeholders.append(f"{_quote_identifier(col)} {op} NULL")
|
|
1971
1984
|
else:
|
|
1972
|
-
placeholders.append(f"{col} {op} ?")
|
|
1985
|
+
placeholders.append(f"{_quote_identifier(col)} {op} ?")
|
|
1973
1986
|
params.append(val)
|
|
1974
1987
|
sql.append("WHERE " + " AND ".join(placeholders))
|
|
1975
1988
|
|
|
@@ -2102,7 +2115,14 @@ class AsyncSQLiteResource(Resource):
|
|
|
2102
2115
|
response = await self.client.request(
|
|
2103
2116
|
"GET", f"/resources/sqlite/{self.resource.name}/describe"
|
|
2104
2117
|
)
|
|
2105
|
-
|
|
2118
|
+
try:
|
|
2119
|
+
return DescribeResponse(**response.json())
|
|
2120
|
+
except json.JSONDecodeError as e:
|
|
2121
|
+
raise ValueError(
|
|
2122
|
+
f"Failed to parse JSON response from SQLite describe endpoint. "
|
|
2123
|
+
f"Status: {response.status_code}, "
|
|
2124
|
+
f"Response text: {response.text[:500]}"
|
|
2125
|
+
) from e
|
|
2106
2126
|
|
|
2107
2127
|
async def _describe_direct(self) -> DescribeResponse:
|
|
2108
2128
|
"""Describe database schema from local file or in-memory database."""
|
|
@@ -2122,7 +2142,7 @@ class AsyncSQLiteResource(Resource):
|
|
|
2122
2142
|
tables = []
|
|
2123
2143
|
for table_name in table_names:
|
|
2124
2144
|
# Get table info
|
|
2125
|
-
cursor.execute(f"PRAGMA table_info({table_name})")
|
|
2145
|
+
cursor.execute(f"PRAGMA table_info({_quote_identifier(table_name)})")
|
|
2126
2146
|
columns = cursor.fetchall()
|
|
2127
2147
|
|
|
2128
2148
|
# Get CREATE TABLE SQL
|
|
@@ -2182,8 +2202,114 @@ class AsyncSQLiteResource(Resource):
|
|
|
2182
2202
|
if self._mode == "direct":
|
|
2183
2203
|
return await self._query_direct(query, args, read_only)
|
|
2184
2204
|
else:
|
|
2205
|
+
# Check if this is a PRAGMA query - HTTP endpoints don't support PRAGMA
|
|
2206
|
+
query_stripped = query.strip().upper()
|
|
2207
|
+
if query_stripped.startswith("PRAGMA"):
|
|
2208
|
+
return await self._handle_pragma_query_http(query, args)
|
|
2185
2209
|
return await self._query_http(query, args, read_only)
|
|
2186
2210
|
|
|
2211
|
+
async def _handle_pragma_query_http(
|
|
2212
|
+
self, query: str, args: Optional[List[Any]] = None
|
|
2213
|
+
) -> QueryResponse:
|
|
2214
|
+
"""Handle PRAGMA queries in HTTP mode by using the describe endpoint."""
|
|
2215
|
+
query_upper = query.strip().upper()
|
|
2216
|
+
|
|
2217
|
+
# Extract table name from PRAGMA table_info(table_name)
|
|
2218
|
+
if "TABLE_INFO" in query_upper:
|
|
2219
|
+
# Match: PRAGMA table_info("table") or PRAGMA table_info(table)
|
|
2220
|
+
match = re.search(r'TABLE_INFO\s*\(\s*"([^"]+)"\s*\)', query, re.IGNORECASE)
|
|
2221
|
+
if not match:
|
|
2222
|
+
match = re.search(r"TABLE_INFO\s*\(\s*'([^']+)'\s*\)", query, re.IGNORECASE)
|
|
2223
|
+
if not match:
|
|
2224
|
+
match = re.search(r'TABLE_INFO\s*\(\s*([^\s\)]+)\s*\)', query, re.IGNORECASE)
|
|
2225
|
+
|
|
2226
|
+
if match:
|
|
2227
|
+
table_name = match.group(1)
|
|
2228
|
+
|
|
2229
|
+
# Use the describe endpoint to get schema
|
|
2230
|
+
describe_response = await self.describe()
|
|
2231
|
+
if not describe_response.success or not describe_response.tables:
|
|
2232
|
+
return QueryResponse(
|
|
2233
|
+
success=False,
|
|
2234
|
+
columns=None,
|
|
2235
|
+
rows=None,
|
|
2236
|
+
error="Failed to get schema information",
|
|
2237
|
+
message="PRAGMA query failed: could not retrieve schema"
|
|
2238
|
+
)
|
|
2239
|
+
|
|
2240
|
+
# Find the table in the schema
|
|
2241
|
+
table_schema = None
|
|
2242
|
+
for table in describe_response.tables:
|
|
2243
|
+
# Handle both dict and TableSchema objects
|
|
2244
|
+
table_name_in_schema = table.name if hasattr(table, 'name') else table.get("name")
|
|
2245
|
+
if table_name_in_schema == table_name:
|
|
2246
|
+
table_schema = table
|
|
2247
|
+
break
|
|
2248
|
+
|
|
2249
|
+
if not table_schema:
|
|
2250
|
+
return QueryResponse(
|
|
2251
|
+
success=False,
|
|
2252
|
+
columns=None,
|
|
2253
|
+
rows=None,
|
|
2254
|
+
error=f"Table '{table_name}' not found",
|
|
2255
|
+
message=f"PRAGMA query failed: table '{table_name}' not found"
|
|
2256
|
+
)
|
|
2257
|
+
|
|
2258
|
+
# Get columns from table schema
|
|
2259
|
+
columns = table_schema.columns if hasattr(table_schema, 'columns') else table_schema.get("columns")
|
|
2260
|
+
if not columns:
|
|
2261
|
+
return QueryResponse(
|
|
2262
|
+
success=False,
|
|
2263
|
+
columns=None,
|
|
2264
|
+
rows=None,
|
|
2265
|
+
error=f"Table '{table_name}' has no columns",
|
|
2266
|
+
message=f"PRAGMA query failed: table '{table_name}' has no columns"
|
|
2267
|
+
)
|
|
2268
|
+
|
|
2269
|
+
# Convert schema to PRAGMA table_info format
|
|
2270
|
+
# Format: (cid, name, type, notnull, dflt_value, pk)
|
|
2271
|
+
rows = []
|
|
2272
|
+
for idx, col in enumerate(columns):
|
|
2273
|
+
# Handle both dict and object column definitions
|
|
2274
|
+
if isinstance(col, dict):
|
|
2275
|
+
col_name = col["name"]
|
|
2276
|
+
col_type = col.get("type", "")
|
|
2277
|
+
col_notnull = col.get("notnull", False)
|
|
2278
|
+
col_default = col.get("default_value")
|
|
2279
|
+
col_pk = col.get("pk", 0)
|
|
2280
|
+
else:
|
|
2281
|
+
col_name = col.name if hasattr(col, 'name') else str(col)
|
|
2282
|
+
col_type = getattr(col, 'type', "")
|
|
2283
|
+
col_notnull = getattr(col, 'notnull', False)
|
|
2284
|
+
col_default = getattr(col, 'default_value', None)
|
|
2285
|
+
col_pk = getattr(col, 'pk', 0)
|
|
2286
|
+
|
|
2287
|
+
row = (
|
|
2288
|
+
idx, # cid
|
|
2289
|
+
col_name, # name
|
|
2290
|
+
col_type, # type
|
|
2291
|
+
1 if col_notnull else 0, # notnull
|
|
2292
|
+
col_default, # dflt_value
|
|
2293
|
+
col_pk # pk
|
|
2294
|
+
)
|
|
2295
|
+
rows.append(row)
|
|
2296
|
+
|
|
2297
|
+
return QueryResponse(
|
|
2298
|
+
success=True,
|
|
2299
|
+
columns=["cid", "name", "type", "notnull", "dflt_value", "pk"],
|
|
2300
|
+
rows=rows,
|
|
2301
|
+
message="PRAGMA query executed successfully via describe endpoint"
|
|
2302
|
+
)
|
|
2303
|
+
|
|
2304
|
+
# For other PRAGMA queries, return an error indicating they're not supported
|
|
2305
|
+
return QueryResponse(
|
|
2306
|
+
success=False,
|
|
2307
|
+
columns=None,
|
|
2308
|
+
rows=None,
|
|
2309
|
+
error="PRAGMA query not supported in HTTP mode",
|
|
2310
|
+
message=f"PRAGMA query '{query}' is not supported via HTTP API"
|
|
2311
|
+
)
|
|
2312
|
+
|
|
2187
2313
|
async def _query_http(
|
|
2188
2314
|
self, query: str, args: Optional[List[Any]] = None, read_only: bool = True
|
|
2189
2315
|
) -> QueryResponse:
|
|
@@ -6,6 +6,8 @@ from datetime import datetime
|
|
|
6
6
|
import tempfile
|
|
7
7
|
import sqlite3
|
|
8
8
|
import os
|
|
9
|
+
import re
|
|
10
|
+
import json
|
|
9
11
|
|
|
10
12
|
from typing import TYPE_CHECKING
|
|
11
13
|
|
|
@@ -2159,7 +2161,14 @@ class SQLiteResource(Resource):
|
|
|
2159
2161
|
response = self.client.request(
|
|
2160
2162
|
"GET", f"/resources/sqlite/{self.resource.name}/describe"
|
|
2161
2163
|
)
|
|
2162
|
-
|
|
2164
|
+
try:
|
|
2165
|
+
return DescribeResponse(**response.json())
|
|
2166
|
+
except json.JSONDecodeError as e:
|
|
2167
|
+
raise ValueError(
|
|
2168
|
+
f"Failed to parse JSON response from SQLite describe endpoint. "
|
|
2169
|
+
f"Status: {response.status_code}, "
|
|
2170
|
+
f"Response text: {response.text[:500]}"
|
|
2171
|
+
) from e
|
|
2163
2172
|
|
|
2164
2173
|
def _describe_direct(self) -> DescribeResponse:
|
|
2165
2174
|
"""Describe database schema from local file or in-memory database."""
|
|
@@ -2234,8 +2243,114 @@ class SQLiteResource(Resource):
|
|
|
2234
2243
|
if self._mode == "direct":
|
|
2235
2244
|
return self._query_direct(query, args, read_only)
|
|
2236
2245
|
else:
|
|
2246
|
+
# Check if this is a PRAGMA query - HTTP endpoints don't support PRAGMA
|
|
2247
|
+
query_stripped = query.strip().upper()
|
|
2248
|
+
if query_stripped.startswith("PRAGMA"):
|
|
2249
|
+
return self._handle_pragma_query_http(query, args)
|
|
2237
2250
|
return self._query_http(query, args, read_only)
|
|
2238
2251
|
|
|
2252
|
+
def _handle_pragma_query_http(
|
|
2253
|
+
self, query: str, args: Optional[List[Any]] = None
|
|
2254
|
+
) -> QueryResponse:
|
|
2255
|
+
"""Handle PRAGMA queries in HTTP mode by using the describe endpoint."""
|
|
2256
|
+
query_upper = query.strip().upper()
|
|
2257
|
+
|
|
2258
|
+
# Extract table name from PRAGMA table_info(table_name)
|
|
2259
|
+
if "TABLE_INFO" in query_upper:
|
|
2260
|
+
# Match: PRAGMA table_info("table") or PRAGMA table_info(table)
|
|
2261
|
+
match = re.search(r'TABLE_INFO\s*\(\s*"([^"]+)"\s*\)', query, re.IGNORECASE)
|
|
2262
|
+
if not match:
|
|
2263
|
+
match = re.search(r"TABLE_INFO\s*\(\s*'([^']+)'\s*\)", query, re.IGNORECASE)
|
|
2264
|
+
if not match:
|
|
2265
|
+
match = re.search(r'TABLE_INFO\s*\(\s*([^\s\)]+)\s*\)', query, re.IGNORECASE)
|
|
2266
|
+
|
|
2267
|
+
if match:
|
|
2268
|
+
table_name = match.group(1)
|
|
2269
|
+
|
|
2270
|
+
# Use the describe endpoint to get schema
|
|
2271
|
+
describe_response = self.describe()
|
|
2272
|
+
if not describe_response.success or not describe_response.tables:
|
|
2273
|
+
return QueryResponse(
|
|
2274
|
+
success=False,
|
|
2275
|
+
columns=None,
|
|
2276
|
+
rows=None,
|
|
2277
|
+
error="Failed to get schema information",
|
|
2278
|
+
message="PRAGMA query failed: could not retrieve schema"
|
|
2279
|
+
)
|
|
2280
|
+
|
|
2281
|
+
# Find the table in the schema
|
|
2282
|
+
table_schema = None
|
|
2283
|
+
for table in describe_response.tables:
|
|
2284
|
+
# Handle both dict and TableSchema objects
|
|
2285
|
+
table_name_in_schema = table.name if hasattr(table, 'name') else table.get("name")
|
|
2286
|
+
if table_name_in_schema == table_name:
|
|
2287
|
+
table_schema = table
|
|
2288
|
+
break
|
|
2289
|
+
|
|
2290
|
+
if not table_schema:
|
|
2291
|
+
return QueryResponse(
|
|
2292
|
+
success=False,
|
|
2293
|
+
columns=None,
|
|
2294
|
+
rows=None,
|
|
2295
|
+
error=f"Table '{table_name}' not found",
|
|
2296
|
+
message=f"PRAGMA query failed: table '{table_name}' not found"
|
|
2297
|
+
)
|
|
2298
|
+
|
|
2299
|
+
# Get columns from table schema
|
|
2300
|
+
columns = table_schema.columns if hasattr(table_schema, 'columns') else table_schema.get("columns")
|
|
2301
|
+
if not columns:
|
|
2302
|
+
return QueryResponse(
|
|
2303
|
+
success=False,
|
|
2304
|
+
columns=None,
|
|
2305
|
+
rows=None,
|
|
2306
|
+
error=f"Table '{table_name}' has no columns",
|
|
2307
|
+
message=f"PRAGMA query failed: table '{table_name}' has no columns"
|
|
2308
|
+
)
|
|
2309
|
+
|
|
2310
|
+
# Convert schema to PRAGMA table_info format
|
|
2311
|
+
# Format: (cid, name, type, notnull, dflt_value, pk)
|
|
2312
|
+
rows = []
|
|
2313
|
+
for idx, col in enumerate(columns):
|
|
2314
|
+
# Handle both dict and object column definitions
|
|
2315
|
+
if isinstance(col, dict):
|
|
2316
|
+
col_name = col["name"]
|
|
2317
|
+
col_type = col.get("type", "")
|
|
2318
|
+
col_notnull = col.get("notnull", False)
|
|
2319
|
+
col_default = col.get("default_value")
|
|
2320
|
+
col_pk = col.get("pk", 0)
|
|
2321
|
+
else:
|
|
2322
|
+
col_name = col.name if hasattr(col, 'name') else str(col)
|
|
2323
|
+
col_type = getattr(col, 'type', "")
|
|
2324
|
+
col_notnull = getattr(col, 'notnull', False)
|
|
2325
|
+
col_default = getattr(col, 'default_value', None)
|
|
2326
|
+
col_pk = getattr(col, 'pk', 0)
|
|
2327
|
+
|
|
2328
|
+
row = (
|
|
2329
|
+
idx, # cid
|
|
2330
|
+
col_name, # name
|
|
2331
|
+
col_type, # type
|
|
2332
|
+
1 if col_notnull else 0, # notnull
|
|
2333
|
+
col_default, # dflt_value
|
|
2334
|
+
col_pk # pk
|
|
2335
|
+
)
|
|
2336
|
+
rows.append(row)
|
|
2337
|
+
|
|
2338
|
+
return QueryResponse(
|
|
2339
|
+
success=True,
|
|
2340
|
+
columns=["cid", "name", "type", "notnull", "dflt_value", "pk"],
|
|
2341
|
+
rows=rows,
|
|
2342
|
+
message="PRAGMA query executed successfully via describe endpoint"
|
|
2343
|
+
)
|
|
2344
|
+
|
|
2345
|
+
# For other PRAGMA queries, return an error indicating they're not supported
|
|
2346
|
+
return QueryResponse(
|
|
2347
|
+
success=False,
|
|
2348
|
+
columns=None,
|
|
2349
|
+
rows=None,
|
|
2350
|
+
error="PRAGMA query not supported in HTTP mode",
|
|
2351
|
+
message=f"PRAGMA query '{query}' is not supported via HTTP API"
|
|
2352
|
+
)
|
|
2353
|
+
|
|
2239
2354
|
def _query_http(
|
|
2240
2355
|
self, query: str, args: Optional[List[Any]] = None, read_only: bool = True
|
|
2241
2356
|
) -> QueryResponse:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|