fleet-python 0.2.70__tar.gz → 0.2.72__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 (89) hide show
  1. {fleet_python-0.2.70/fleet_python.egg-info → fleet_python-0.2.72}/PKG-INFO +1 -1
  2. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/__init__.py +1 -1
  3. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/_async/__init__.py +1 -1
  4. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/_async/base.py +1 -1
  5. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/_async/client.py +163 -6
  6. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/_async/instance/client.py +19 -4
  7. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/_async/resources/sqlite.py +150 -1
  8. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/_async/tasks.py +46 -2
  9. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/base.py +1 -1
  10. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/client.py +183 -15
  11. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/instance/client.py +20 -5
  12. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/resources/sqlite.py +143 -1
  13. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/tasks.py +46 -2
  14. {fleet_python-0.2.70 → fleet_python-0.2.72/fleet_python.egg-info}/PKG-INFO +1 -1
  15. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet_python.egg-info/SOURCES.txt +4 -0
  16. {fleet_python-0.2.70 → fleet_python-0.2.72}/pyproject.toml +2 -1
  17. fleet_python-0.2.72/tests/test_app_method.py +85 -0
  18. fleet_python-0.2.72/tests/test_instance_dispatch.py +607 -0
  19. fleet_python-0.2.72/tests/test_sqlite_resource_dual_mode.py +263 -0
  20. fleet_python-0.2.72/tests/test_sqlite_shared_memory_behavior.py +117 -0
  21. {fleet_python-0.2.70 → fleet_python-0.2.72}/LICENSE +0 -0
  22. {fleet_python-0.2.70 → fleet_python-0.2.72}/README.md +0 -0
  23. {fleet_python-0.2.70 → fleet_python-0.2.72}/examples/diff_example.py +0 -0
  24. {fleet_python-0.2.70 → fleet_python-0.2.72}/examples/dsl_example.py +0 -0
  25. {fleet_python-0.2.70 → fleet_python-0.2.72}/examples/example.py +0 -0
  26. {fleet_python-0.2.70 → fleet_python-0.2.72}/examples/exampleResume.py +0 -0
  27. {fleet_python-0.2.70 → fleet_python-0.2.72}/examples/example_account.py +0 -0
  28. {fleet_python-0.2.70 → fleet_python-0.2.72}/examples/example_action_log.py +0 -0
  29. {fleet_python-0.2.70 → fleet_python-0.2.72}/examples/example_client.py +0 -0
  30. {fleet_python-0.2.70 → fleet_python-0.2.72}/examples/example_mcp_anthropic.py +0 -0
  31. {fleet_python-0.2.70 → fleet_python-0.2.72}/examples/example_mcp_openai.py +0 -0
  32. {fleet_python-0.2.70 → fleet_python-0.2.72}/examples/example_sync.py +0 -0
  33. {fleet_python-0.2.70 → fleet_python-0.2.72}/examples/example_task.py +0 -0
  34. {fleet_python-0.2.70 → fleet_python-0.2.72}/examples/example_tasks.py +0 -0
  35. {fleet_python-0.2.70 → fleet_python-0.2.72}/examples/example_verifier.py +0 -0
  36. {fleet_python-0.2.70 → fleet_python-0.2.72}/examples/export_tasks.py +0 -0
  37. {fleet_python-0.2.70 → fleet_python-0.2.72}/examples/gemini_example.py +0 -0
  38. {fleet_python-0.2.70 → fleet_python-0.2.72}/examples/import_tasks.py +0 -0
  39. {fleet_python-0.2.70 → fleet_python-0.2.72}/examples/json_tasks_example.py +0 -0
  40. {fleet_python-0.2.70 → fleet_python-0.2.72}/examples/nova_act_example.py +0 -0
  41. {fleet_python-0.2.70 → fleet_python-0.2.72}/examples/openai_example.py +0 -0
  42. {fleet_python-0.2.70 → fleet_python-0.2.72}/examples/openai_simple_example.py +0 -0
  43. {fleet_python-0.2.70 → fleet_python-0.2.72}/examples/query_builder_example.py +0 -0
  44. {fleet_python-0.2.70 → fleet_python-0.2.72}/examples/quickstart.py +0 -0
  45. {fleet_python-0.2.70 → fleet_python-0.2.72}/examples/test_cdp_logging.py +0 -0
  46. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/_async/env/__init__.py +0 -0
  47. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/_async/env/client.py +0 -0
  48. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/_async/exceptions.py +0 -0
  49. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/_async/global_client.py +0 -0
  50. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/_async/instance/__init__.py +0 -0
  51. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/_async/instance/base.py +0 -0
  52. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/_async/models.py +0 -0
  53. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/_async/resources/__init__.py +0 -0
  54. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/_async/resources/base.py +0 -0
  55. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/_async/resources/browser.py +0 -0
  56. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/_async/resources/mcp.py +0 -0
  57. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/_async/verifiers/__init__.py +0 -0
  58. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/_async/verifiers/bundler.py +0 -0
  59. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/_async/verifiers/verifier.py +0 -0
  60. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/config.py +0 -0
  61. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/env/__init__.py +0 -0
  62. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/env/client.py +0 -0
  63. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/exceptions.py +0 -0
  64. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/global_client.py +0 -0
  65. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/instance/__init__.py +0 -0
  66. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/instance/base.py +0 -0
  67. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/instance/models.py +0 -0
  68. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/models.py +0 -0
  69. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/resources/__init__.py +0 -0
  70. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/resources/base.py +0 -0
  71. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/resources/browser.py +0 -0
  72. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/resources/mcp.py +0 -0
  73. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/types.py +0 -0
  74. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/verifiers/__init__.py +0 -0
  75. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/verifiers/bundler.py +0 -0
  76. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/verifiers/code.py +0 -0
  77. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/verifiers/db.py +0 -0
  78. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/verifiers/decorator.py +0 -0
  79. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/verifiers/parse.py +0 -0
  80. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/verifiers/sql_differ.py +0 -0
  81. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet/verifiers/verifier.py +0 -0
  82. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet_python.egg-info/dependency_links.txt +0 -0
  83. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet_python.egg-info/requires.txt +0 -0
  84. {fleet_python-0.2.70 → fleet_python-0.2.72}/fleet_python.egg-info/top_level.txt +0 -0
  85. {fleet_python-0.2.70 → fleet_python-0.2.72}/scripts/fix_sync_imports.py +0 -0
  86. {fleet_python-0.2.70 → fleet_python-0.2.72}/scripts/unasync.py +0 -0
  87. {fleet_python-0.2.70 → fleet_python-0.2.72}/setup.cfg +0 -0
  88. {fleet_python-0.2.70 → fleet_python-0.2.72}/tests/__init__.py +0 -0
  89. {fleet_python-0.2.70 → fleet_python-0.2.72}/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.70
3
+ Version: 0.2.72
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.70"
76
+ __version__ = "0.2.71"
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.70"
47
+ __version__ = "0.2.71"
48
48
 
49
49
  __all__ = [
50
50
  # Core classes
@@ -25,7 +25,7 @@ from .exceptions import (
25
25
  try:
26
26
  from .. import __version__
27
27
  except ImportError:
28
- __version__ = "0.2.70"
28
+ __version__ = "0.2.71"
29
29
 
30
30
  logger = logging.getLogger(__name__)
31
31
 
@@ -21,7 +21,7 @@ import httpx
21
21
  import json
22
22
  import logging
23
23
  import os
24
- from typing import List, Optional, Dict, Any, TYPE_CHECKING
24
+ from typing import List, Optional, Dict, Any, TYPE_CHECKING, Union
25
25
 
26
26
  from .base import EnvironmentBase, AsyncWrapper
27
27
  from ..models import (
@@ -49,6 +49,11 @@ from .instance import (
49
49
  ResetResponse,
50
50
  ExecuteFunctionResponse,
51
51
  )
52
+ from ..instance.models import (
53
+ Resource as ResourceModel,
54
+ ResourceType,
55
+ ResourceMode,
56
+ )
52
57
  from ..config import (
53
58
  DEFAULT_MAX_RETRIES,
54
59
  DEFAULT_TIMEOUT,
@@ -310,11 +315,163 @@ class AsyncFleet:
310
315
  for instance_data in response.json()
311
316
  ]
312
317
 
313
- async def instance(self, instance_id: str) -> AsyncEnv:
314
- response = await self.client.request("GET", f"/v1/env/instances/{instance_id}")
315
- instance = AsyncEnv(client=self.client, **response.json())
316
- await instance.instance.load()
317
- return instance
318
+ async def instance(self, instance_id: Union[str, Dict[str, str]]) -> AsyncEnv:
319
+ """Create or connect to an environment instance.
320
+
321
+ Supports three modes based on input type:
322
+ 1. dict: Local filesystem mode - {"current": "./data.db", "seed": "./seed.db"}
323
+ 2. str starting with http:// or https://: Localhost/URL mode
324
+ 3. str (other): Remote cloud instance mode
325
+
326
+ Args:
327
+ instance_id: Instance identifier (str), URL (str starting with http://),
328
+ or local db mapping (dict)
329
+
330
+ Returns:
331
+ AsyncEnv: Environment instance
332
+ """
333
+ # Local filesystem mode - dict of resource names to file paths
334
+ if isinstance(instance_id, dict):
335
+ return self._create_local_instance(instance_id)
336
+
337
+ # Localhost/direct URL mode - string starting with http:// or https://
338
+ elif isinstance(instance_id, str) and instance_id.startswith(("http://", "https://")):
339
+ return self._create_url_instance(instance_id)
340
+
341
+ # Remote mode - existing behavior
342
+ else:
343
+ response = await self.client.request("GET", f"/v1/env/instances/{instance_id}")
344
+ instance = AsyncEnv(client=self.client, **response.json())
345
+ await instance.instance.load()
346
+ return instance
347
+
348
+ def _create_url_instance(self, base_url: str) -> AsyncEnv:
349
+ """Create instance connected to a direct URL (localhost or custom).
350
+
351
+ Args:
352
+ base_url: URL of the instance manager API
353
+
354
+ Returns:
355
+ AsyncEnv: Environment instance configured for URL mode
356
+ """
357
+ instance_client = AsyncInstanceClient(url=base_url, httpx_client=self._httpx_client)
358
+
359
+ # Create a minimal environment for URL mode
360
+ env = AsyncEnv(
361
+ client=self.client,
362
+ instance_id=base_url,
363
+ env_key="localhost",
364
+ version="",
365
+ status="running",
366
+ subdomain="localhost",
367
+ created_at="",
368
+ updated_at="",
369
+ terminated_at=None,
370
+ team_id="",
371
+ region="localhost",
372
+ env_variables=None,
373
+ data_key=None,
374
+ data_version=None,
375
+ urls=None,
376
+ health=None,
377
+ )
378
+ env._instance = instance_client
379
+ return env
380
+
381
+ @staticmethod
382
+ def _normalize_db_path(path: str) -> tuple[str, bool]:
383
+ """Normalize database path and detect if it's in-memory.
384
+
385
+ Args:
386
+ path: Database path - can be:
387
+ - File path: "./data.db"
388
+ - Plain memory: ":memory:"
389
+ - Named memory: ":memory:namespace"
390
+ - URI: "file:name?mode=memory&cache=shared"
391
+
392
+ Returns:
393
+ Tuple of (normalized_path, is_memory)
394
+ """
395
+ import uuid
396
+ import sqlite3
397
+
398
+ if path == ":memory:":
399
+ # Plain :memory: - create unique namespace
400
+ name = f"mem_{uuid.uuid4().hex[:8]}"
401
+ return f"file:{name}?mode=memory&cache=shared", True
402
+ elif path.startswith(":memory:"):
403
+ # Named memory: :memory:current -> file:current?mode=memory&cache=shared
404
+ namespace = path[8:] # Remove ":memory:" prefix
405
+ return f"file:{namespace}?mode=memory&cache=shared", True
406
+ elif "mode=memory" in path:
407
+ # Already a proper memory URI
408
+ return path, True
409
+ else:
410
+ # Regular file path
411
+ return path, False
412
+
413
+ def _create_local_instance(self, dbs: Dict[str, str]) -> AsyncEnv:
414
+ """Create instance with local file-based or in-memory SQLite resources.
415
+
416
+ Args:
417
+ dbs: Map of resource names to paths (e.g., {"current": "./data.db"} or
418
+ {"current": ":memory:current"})
419
+
420
+ Returns:
421
+ AsyncEnv: Environment instance configured for local mode
422
+ """
423
+ import sqlite3
424
+
425
+ instance_client = AsyncInstanceClient(url="local://", httpx_client=None)
426
+ instance_client._resources = [] # Mark as loaded
427
+ instance_client._memory_anchors = {} # Store anchor connections for in-memory DBs
428
+
429
+ # Store creation parameters for local AsyncSQLiteResources
430
+ # This allows db() to create new instances each time (matching HTTP mode behavior)
431
+ for name, path in dbs.items():
432
+ # Normalize path and detect if it's in-memory
433
+ normalized_path, is_memory = self._normalize_db_path(path)
434
+
435
+ # Create anchor connection for in-memory databases
436
+ # This keeps the database alive as long as the env exists
437
+ if is_memory:
438
+ anchor_conn = sqlite3.connect(normalized_path, uri=True)
439
+ instance_client._memory_anchors[name] = anchor_conn
440
+
441
+ resource_model = ResourceModel(
442
+ name=name,
443
+ type=ResourceType.db,
444
+ mode=ResourceMode.rw,
445
+ label=f"Local: {path}",
446
+ )
447
+ instance_client._resources_state[ResourceType.db.value][name] = {
448
+ 'type': 'local',
449
+ 'resource_model': resource_model,
450
+ 'db_path': normalized_path,
451
+ 'is_memory': is_memory
452
+ }
453
+
454
+ # Create a minimal environment for local mode
455
+ env = AsyncEnv(
456
+ client=self.client,
457
+ instance_id="local",
458
+ env_key="local",
459
+ version="",
460
+ status="running",
461
+ subdomain="local",
462
+ created_at="",
463
+ updated_at="",
464
+ terminated_at=None,
465
+ team_id="",
466
+ region="local",
467
+ env_variables=None,
468
+ data_key=None,
469
+ data_version=None,
470
+ urls=None,
471
+ health=None,
472
+ )
473
+ env._instance = instance_client
474
+ return env
318
475
 
319
476
  async def check_bundle_exists(self, bundle_hash: str) -> VerifiersCheckResponse:
320
477
  return await _check_bundle_exists(self.client, bundle_hash)
@@ -85,9 +85,17 @@ class AsyncInstanceClient:
85
85
  Returns:
86
86
  An SQLite database resource for the given database name
87
87
  """
88
- return AsyncSQLiteResource(
89
- self._resources_state[ResourceType.db.value][name], self.client
90
- )
88
+ resource_info = self._resources_state[ResourceType.db.value][name]
89
+ # Local mode - resource_info is a dict with creation parameters
90
+ if isinstance(resource_info, dict) and resource_info.get('type') == 'local':
91
+ # Create new instance each time (matching HTTP mode behavior)
92
+ return AsyncSQLiteResource(
93
+ resource_info['resource_model'],
94
+ client=None,
95
+ db_path=resource_info['db_path']
96
+ )
97
+ # HTTP mode - resource_info is a ResourceModel, create new wrapper
98
+ return AsyncSQLiteResource(resource_info, self.client)
91
99
 
92
100
  def browser(self, name: str) -> AsyncBrowserResource:
93
101
  return AsyncBrowserResource(
@@ -177,10 +185,17 @@ class AsyncInstanceClient:
177
185
  response = await self.client.request("GET", "/health")
178
186
  return HealthResponse(**response.json())
179
187
 
188
+ def close(self):
189
+ """Close anchor connections for in-memory databases."""
190
+ if hasattr(self, '_memory_anchors'):
191
+ for conn in self._memory_anchors.values():
192
+ conn.close()
193
+ self._memory_anchors.clear()
194
+
180
195
  async def __aenter__(self):
181
196
  """Async context manager entry."""
182
197
  return self
183
198
 
184
199
  async def __aexit__(self, exc_type, exc_val, exc_tb):
185
200
  """Async context manager exit."""
186
- await self.close()
201
+ self.close()
@@ -6,6 +6,7 @@ from datetime import datetime
6
6
  import tempfile
7
7
  import sqlite3
8
8
  import os
9
+ import asyncio
9
10
 
10
11
  from typing import TYPE_CHECKING
11
12
 
@@ -679,17 +680,100 @@ class AsyncQueryBuilder:
679
680
 
680
681
 
681
682
  class AsyncSQLiteResource(Resource):
682
- def __init__(self, resource: ResourceModel, client: "AsyncWrapper"):
683
+ def __init__(
684
+ self,
685
+ resource: ResourceModel,
686
+ client: Optional["AsyncWrapper"] = None,
687
+ db_path: Optional[str] = None,
688
+ ):
683
689
  super().__init__(resource)
684
690
  self.client = client
691
+ self.db_path = db_path
692
+ self._mode = "direct" if db_path else "http"
693
+
694
+ @property
695
+ def mode(self) -> str:
696
+ """Return the mode of this resource: 'direct' (local file) or 'http' (remote API)."""
697
+ return self._mode
685
698
 
686
699
  async def describe(self) -> DescribeResponse:
687
700
  """Describe the SQLite database schema."""
701
+ if self._mode == "direct":
702
+ return await self._describe_direct()
703
+ else:
704
+ return await self._describe_http()
705
+
706
+ async def _describe_http(self) -> DescribeResponse:
707
+ """Describe database schema via HTTP API."""
688
708
  response = await self.client.request(
689
709
  "GET", f"/resources/sqlite/{self.resource.name}/describe"
690
710
  )
691
711
  return DescribeResponse(**response.json())
692
712
 
713
+ async def _describe_direct(self) -> DescribeResponse:
714
+ """Describe database schema from local file or in-memory database."""
715
+ def _sync_describe():
716
+ try:
717
+ # Check if we need URI mode (for shared memory databases)
718
+ use_uri = 'mode=memory' in self.db_path
719
+ conn = sqlite3.connect(self.db_path, uri=use_uri)
720
+ cursor = conn.cursor()
721
+
722
+ # Get all tables
723
+ cursor.execute(
724
+ "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
725
+ )
726
+ table_names = [row[0] for row in cursor.fetchall()]
727
+
728
+ tables = []
729
+ for table_name in table_names:
730
+ # Get table info
731
+ cursor.execute(f"PRAGMA table_info({table_name})")
732
+ columns = cursor.fetchall()
733
+
734
+ # Get CREATE TABLE SQL
735
+ cursor.execute(
736
+ f"SELECT sql FROM sqlite_master WHERE type='table' AND name=?",
737
+ (table_name,)
738
+ )
739
+ sql_row = cursor.fetchone()
740
+ create_sql = sql_row[0] if sql_row else ""
741
+
742
+ table_schema = {
743
+ "name": table_name,
744
+ "sql": create_sql,
745
+ "columns": [
746
+ {
747
+ "name": col[1],
748
+ "type": col[2],
749
+ "notnull": bool(col[3]),
750
+ "default_value": col[4],
751
+ "primary_key": col[5] > 0,
752
+ }
753
+ for col in columns
754
+ ],
755
+ }
756
+ tables.append(table_schema)
757
+
758
+ conn.close()
759
+
760
+ return DescribeResponse(
761
+ success=True,
762
+ resource_name=self.resource.name,
763
+ tables=tables,
764
+ message="Schema retrieved from local file",
765
+ )
766
+ except Exception as e:
767
+ return DescribeResponse(
768
+ success=False,
769
+ resource_name=self.resource.name,
770
+ tables=None,
771
+ error=str(e),
772
+ message=f"Failed to describe database: {str(e)}",
773
+ )
774
+
775
+ return await asyncio.to_thread(_sync_describe)
776
+
693
777
  async def query(
694
778
  self, query: str, args: Optional[List[Any]] = None
695
779
  ) -> QueryResponse:
@@ -701,6 +785,15 @@ class AsyncSQLiteResource(Resource):
701
785
  async def _query(
702
786
  self, query: str, args: Optional[List[Any]] = None, read_only: bool = True
703
787
  ) -> QueryResponse:
788
+ if self._mode == "direct":
789
+ return await self._query_direct(query, args, read_only)
790
+ else:
791
+ return await self._query_http(query, args, read_only)
792
+
793
+ async def _query_http(
794
+ self, query: str, args: Optional[List[Any]] = None, read_only: bool = True
795
+ ) -> QueryResponse:
796
+ """Execute query via HTTP API."""
704
797
  request = QueryRequest(query=query, args=args, read_only=read_only)
705
798
  response = await self.client.request(
706
799
  "POST",
@@ -709,6 +802,62 @@ class AsyncSQLiteResource(Resource):
709
802
  )
710
803
  return QueryResponse(**response.json())
711
804
 
805
+ async def _query_direct(
806
+ self, query: str, args: Optional[List[Any]] = None, read_only: bool = True
807
+ ) -> QueryResponse:
808
+ """Execute query directly on local SQLite file or in-memory database."""
809
+ def _sync_query():
810
+ try:
811
+ # Check if we need URI mode (for shared memory databases)
812
+ use_uri = 'mode=memory' in self.db_path
813
+ conn = sqlite3.connect(self.db_path, uri=use_uri)
814
+ cursor = conn.cursor()
815
+
816
+ # Execute the query
817
+ if args:
818
+ cursor.execute(query, args)
819
+ else:
820
+ cursor.execute(query)
821
+
822
+ # For write operations, commit the transaction
823
+ if not read_only:
824
+ conn.commit()
825
+
826
+ # Get column names if available
827
+ columns = [desc[0] for desc in cursor.description] if cursor.description else []
828
+
829
+ # Fetch results for SELECT queries
830
+ rows = []
831
+ rows_affected = 0
832
+ last_insert_id = None
833
+
834
+ if cursor.description: # SELECT query
835
+ rows = cursor.fetchall()
836
+ else: # INSERT/UPDATE/DELETE
837
+ rows_affected = cursor.rowcount
838
+ last_insert_id = cursor.lastrowid if cursor.lastrowid else None
839
+
840
+ conn.close()
841
+
842
+ return QueryResponse(
843
+ success=True,
844
+ columns=columns if columns else None,
845
+ rows=rows if rows else None,
846
+ rows_affected=rows_affected if rows_affected > 0 else None,
847
+ last_insert_id=last_insert_id,
848
+ message="Query executed successfully",
849
+ )
850
+ except Exception as e:
851
+ return QueryResponse(
852
+ success=False,
853
+ columns=None,
854
+ rows=None,
855
+ error=str(e),
856
+ message=f"Query failed: {str(e)}",
857
+ )
858
+
859
+ return await asyncio.to_thread(_sync_query)
860
+
712
861
  def table(self, table_name: str) -> AsyncQueryBuilder:
713
862
  """Create a query builder for the specified table."""
714
863
  return AsyncQueryBuilder(self, table_name)
@@ -296,6 +296,8 @@ def verifier_from_string(
296
296
  try:
297
297
  import inspect
298
298
  import re
299
+ import json
300
+ import string
299
301
  from .verifiers.verifier import AsyncVerifierFunction
300
302
  from fleet.verifiers.code import TASK_SUCCESSFUL_SCORE, TASK_FAILED_SCORE
301
303
  from fleet.verifiers.db import IgnoreConfig
@@ -304,8 +306,44 @@ def verifier_from_string(
304
306
  # Remove lines like: @verifier(key="...")
305
307
  cleaned_code = re.sub(r"@verifier\([^)]*\)\s*\n", "", verifier_func)
306
308
  # Also remove the verifier import if present
307
- cleaned_code = re.sub(r"from fleet import.*verifier.*\n", "", cleaned_code)
308
- cleaned_code = re.sub(r"import.*verifier.*\n", "", cleaned_code)
309
+ # Use MULTILINE flag to match beginning of lines with ^
310
+ cleaned_code = re.sub(r"^from fleet\.verifiers.*import.*verifier.*$\n?", "", cleaned_code, flags=re.MULTILINE)
311
+ cleaned_code = re.sub(r"^from fleet import verifier.*$\n?", "", cleaned_code, flags=re.MULTILINE)
312
+ cleaned_code = re.sub(r"^import fleet\.verifiers.*$\n?", "", cleaned_code, flags=re.MULTILINE)
313
+ cleaned_code = re.sub(r"^import fleet$\n?", "", cleaned_code, flags=re.MULTILINE)
314
+
315
+ # Define helper functions for verifier execution
316
+ _TRANSLATOR = str.maketrans(string.punctuation, " " * len(string.punctuation))
317
+
318
+ def _normalize_text(value: str) -> str:
319
+ text = value.lower().translate(_TRANSLATOR)
320
+ return "".join(text.split())
321
+
322
+ def _stringify_content(content: Any) -> str:
323
+ if isinstance(content, (dict, list)):
324
+ return json.dumps(content, sort_keys=True)
325
+ return str(content)
326
+
327
+ def normalized_contains(target: str, blob: Any) -> bool:
328
+ normalized_target = _normalize_text(target)
329
+ normalized_blob = _normalize_text(_stringify_content(blob))
330
+ return normalized_target in normalized_blob
331
+
332
+ def extract_numbers(text: str) -> list:
333
+ cleaned_text = text.replace(',', '')
334
+ pattern = r'-?\d+\.?\d*'
335
+ matches = re.findall(pattern, cleaned_text)
336
+ return [float(num) for num in matches]
337
+
338
+ def contains_number(text: str, target_number) -> bool:
339
+ numbers = extract_numbers(text)
340
+ try:
341
+ if isinstance(target_number, str):
342
+ target_number = target_number.replace(',', '')
343
+ target = float(target_number)
344
+ except (ValueError, AttributeError):
345
+ return False
346
+ return target in numbers
309
347
 
310
348
  # Create a local namespace for executing the code
311
349
  local_namespace = {
@@ -313,6 +351,12 @@ def verifier_from_string(
313
351
  "TASK_FAILED_SCORE": TASK_FAILED_SCORE,
314
352
  "IgnoreConfig": IgnoreConfig,
315
353
  "Environment": object, # Add Environment type if needed
354
+ "normalized_contains": normalized_contains,
355
+ "extract_numbers": extract_numbers,
356
+ "contains_number": contains_number,
357
+ "json": json,
358
+ "re": re,
359
+ "string": string,
316
360
  }
317
361
 
318
362
  # Execute the cleaned verifier code in the namespace
@@ -25,7 +25,7 @@ from .exceptions import (
25
25
  try:
26
26
  from . import __version__
27
27
  except ImportError:
28
- __version__ = "0.2.70"
28
+ __version__ = "0.2.71"
29
29
 
30
30
  logger = logging.getLogger(__name__)
31
31