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.
Files changed (118) hide show
  1. {fleet_python-0.2.98/fleet_python.egg-info → fleet_python-0.2.100}/PKG-INFO +1 -1
  2. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/__init__.py +1 -1
  3. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/__init__.py +1 -1
  4. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/base.py +1 -1
  5. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/resources/sqlite.py +156 -30
  6. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/base.py +1 -1
  7. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/resources/sqlite.py +116 -1
  8. {fleet_python-0.2.98 → fleet_python-0.2.100/fleet_python.egg-info}/PKG-INFO +1 -1
  9. {fleet_python-0.2.98 → fleet_python-0.2.100}/pyproject.toml +1 -1
  10. {fleet_python-0.2.98 → fleet_python-0.2.100}/LICENSE +0 -0
  11. {fleet_python-0.2.98 → fleet_python-0.2.100}/README.md +0 -0
  12. {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/diff_example.py +0 -0
  13. {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/dsl_example.py +0 -0
  14. {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/example.py +0 -0
  15. {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/exampleResume.py +0 -0
  16. {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/example_account.py +0 -0
  17. {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/example_action_log.py +0 -0
  18. {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/example_client.py +0 -0
  19. {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/example_mcp_anthropic.py +0 -0
  20. {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/example_mcp_openai.py +0 -0
  21. {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/example_sync.py +0 -0
  22. {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/example_task.py +0 -0
  23. {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/example_tasks.py +0 -0
  24. {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/example_verifier.py +0 -0
  25. {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/export_tasks.py +0 -0
  26. {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/fetch_tasks.py +0 -0
  27. {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/gemini_example.py +0 -0
  28. {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/import_tasks.py +0 -0
  29. {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/iterate_verifiers.py +0 -0
  30. {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/json_tasks_example.py +0 -0
  31. {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/nova_act_example.py +0 -0
  32. {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/openai_example.py +0 -0
  33. {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/openai_simple_example.py +0 -0
  34. {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/query_builder_example.py +0 -0
  35. {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/quickstart.py +0 -0
  36. {fleet_python-0.2.98 → fleet_python-0.2.100}/examples/test_cdp_logging.py +0 -0
  37. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/client.py +0 -0
  38. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/env/__init__.py +0 -0
  39. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/env/client.py +0 -0
  40. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/exceptions.py +0 -0
  41. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/global_client.py +0 -0
  42. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/instance/__init__.py +0 -0
  43. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/instance/base.py +0 -0
  44. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/instance/client.py +0 -0
  45. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/models.py +0 -0
  46. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/resources/__init__.py +0 -0
  47. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/resources/api.py +0 -0
  48. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/resources/base.py +0 -0
  49. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/resources/browser.py +0 -0
  50. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/resources/mcp.py +0 -0
  51. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/tasks.py +0 -0
  52. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/verifiers/__init__.py +0 -0
  53. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/verifiers/bundler.py +0 -0
  54. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/_async/verifiers/verifier.py +0 -0
  55. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/agent/__init__.py +0 -0
  56. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/agent/gemini_cua/Dockerfile +0 -0
  57. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/agent/gemini_cua/__init__.py +0 -0
  58. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/agent/gemini_cua/agent.py +0 -0
  59. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/agent/gemini_cua/mcp/main.py +0 -0
  60. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/agent/gemini_cua/mcp_server/__init__.py +0 -0
  61. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/agent/gemini_cua/mcp_server/main.py +0 -0
  62. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/agent/gemini_cua/mcp_server/tools.py +0 -0
  63. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/agent/gemini_cua/requirements.txt +0 -0
  64. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/agent/gemini_cua/start.sh +0 -0
  65. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/agent/orchestrator.py +0 -0
  66. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/agent/types.py +0 -0
  67. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/agent/utils.py +0 -0
  68. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/cli.py +0 -0
  69. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/client.py +0 -0
  70. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/config.py +0 -0
  71. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/env/__init__.py +0 -0
  72. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/env/client.py +0 -0
  73. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/eval/__init__.py +0 -0
  74. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/eval/uploader.py +0 -0
  75. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/exceptions.py +0 -0
  76. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/global_client.py +0 -0
  77. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/instance/__init__.py +0 -0
  78. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/instance/base.py +0 -0
  79. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/instance/client.py +0 -0
  80. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/instance/models.py +0 -0
  81. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/models.py +0 -0
  82. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/proxy/__init__.py +0 -0
  83. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/proxy/proxy.py +0 -0
  84. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/proxy/whitelist.py +0 -0
  85. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/resources/__init__.py +0 -0
  86. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/resources/api.py +0 -0
  87. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/resources/base.py +0 -0
  88. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/resources/browser.py +0 -0
  89. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/resources/mcp.py +0 -0
  90. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/tasks.py +0 -0
  91. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/types.py +0 -0
  92. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/utils/__init__.py +0 -0
  93. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/utils/http_logging.py +0 -0
  94. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/utils/logging.py +0 -0
  95. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/utils/playwright.py +0 -0
  96. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/verifiers/__init__.py +0 -0
  97. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/verifiers/bundler.py +0 -0
  98. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/verifiers/code.py +0 -0
  99. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/verifiers/db.py +0 -0
  100. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/verifiers/decorator.py +0 -0
  101. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/verifiers/parse.py +0 -0
  102. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/verifiers/sql_differ.py +0 -0
  103. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet/verifiers/verifier.py +0 -0
  104. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet_python.egg-info/SOURCES.txt +0 -0
  105. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet_python.egg-info/dependency_links.txt +0 -0
  106. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet_python.egg-info/entry_points.txt +0 -0
  107. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet_python.egg-info/requires.txt +0 -0
  108. {fleet_python-0.2.98 → fleet_python-0.2.100}/fleet_python.egg-info/top_level.txt +0 -0
  109. {fleet_python-0.2.98 → fleet_python-0.2.100}/scripts/fix_sync_imports.py +0 -0
  110. {fleet_python-0.2.98 → fleet_python-0.2.100}/scripts/unasync.py +0 -0
  111. {fleet_python-0.2.98 → fleet_python-0.2.100}/setup.cfg +0 -0
  112. {fleet_python-0.2.98 → fleet_python-0.2.100}/tests/__init__.py +0 -0
  113. {fleet_python-0.2.98 → fleet_python-0.2.100}/tests/test_app_method.py +0 -0
  114. {fleet_python-0.2.98 → fleet_python-0.2.100}/tests/test_expect_only.py +0 -0
  115. {fleet_python-0.2.98 → fleet_python-0.2.100}/tests/test_instance_dispatch.py +0 -0
  116. {fleet_python-0.2.98 → fleet_python-0.2.100}/tests/test_sqlite_resource_dual_mode.py +0 -0
  117. {fleet_python-0.2.98 → fleet_python-0.2.100}/tests/test_sqlite_shared_memory_behavior.py +0 -0
  118. {fleet_python-0.2.98 → fleet_python-0.2.100}/tests/test_verifier_from_string.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fleet-python
3
- Version: 0.2.98
3
+ Version: 0.2.100
4
4
  Summary: Python SDK for Fleet environments
5
5
  Author-email: Fleet AI <nic@fleet.so>
6
6
  License: Apache-2.0
@@ -73,7 +73,7 @@ from . import env
73
73
  from . import global_client as _global_client
74
74
  from ._async import global_client as _async_global_client
75
75
 
76
- __version__ = "0.2.98"
76
+ __version__ = "0.2.100"
77
77
 
78
78
  __all__ = [
79
79
  # Core classes
@@ -44,7 +44,7 @@ from ..types import VerifierFunction
44
44
  from .. import env
45
45
  from . import global_client as _async_global_client
46
46
 
47
- __version__ = "0.2.98"
47
+ __version__ = "0.2.100"
48
48
 
49
49
  __all__ = [
50
50
  # Core classes
@@ -26,7 +26,7 @@ from .exceptions import (
26
26
  try:
27
27
  from .. import __version__
28
28
  except ImportError:
29
- __version__ = "0.2.98"
29
+ __version__ = "0.2.100"
30
30
 
31
31
  logger = logging.getLogger(__name__)
32
32
 
@@ -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
- return DescribeResponse(**response.json())
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:
@@ -27,7 +27,7 @@ from .exceptions import (
27
27
  try:
28
28
  from . import __version__
29
29
  except ImportError:
30
- __version__ = "0.2.98"
30
+ __version__ = "0.2.100"
31
31
 
32
32
  logger = logging.getLogger(__name__)
33
33
 
@@ -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
- return DescribeResponse(**response.json())
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fleet-python
3
- Version: 0.2.98
3
+ Version: 0.2.100
4
4
  Summary: Python SDK for Fleet environments
5
5
  Author-email: Fleet AI <nic@fleet.so>
6
6
  License: Apache-2.0
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "fleet-python"
7
7
 
8
- version = "0.2.98"
8
+ version = "0.2.100"
9
9
  description = "Python SDK for Fleet environments"
10
10
  authors = [
11
11
  {name = "Fleet AI", email = "nic@fleet.so"},
File without changes
File without changes
File without changes