fleet-python 0.2.67__tar.gz → 0.2.69b2__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.

Potentially problematic release.


This version of fleet-python might be problematic. Click here for more details.

Files changed (88) hide show
  1. {fleet_python-0.2.67/fleet_python.egg-info → fleet_python-0.2.69b2}/PKG-INFO +1 -1
  2. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/_async/client.py +195 -7
  3. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/_async/env/client.py +29 -3
  4. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/_async/instance/client.py +19 -4
  5. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/_async/resources/sqlite.py +150 -1
  6. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/_async/tasks.py +12 -4
  7. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/client.py +195 -7
  8. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/env/__init__.py +8 -0
  9. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/env/client.py +29 -3
  10. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/instance/client.py +20 -5
  11. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/models.py +2 -0
  12. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/resources/sqlite.py +143 -1
  13. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/tasks.py +12 -4
  14. {fleet_python-0.2.67 → fleet_python-0.2.69b2/fleet_python.egg-info}/PKG-INFO +1 -1
  15. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet_python.egg-info/SOURCES.txt +3 -0
  16. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/pyproject.toml +1 -1
  17. fleet_python-0.2.69b2/tests/test_instance_dispatch.py +607 -0
  18. fleet_python-0.2.69b2/tests/test_sqlite_resource_dual_mode.py +263 -0
  19. fleet_python-0.2.69b2/tests/test_sqlite_shared_memory_behavior.py +117 -0
  20. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/LICENSE +0 -0
  21. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/README.md +0 -0
  22. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/diff_example.py +0 -0
  23. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/dsl_example.py +0 -0
  24. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/example.py +0 -0
  25. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/exampleResume.py +0 -0
  26. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/example_account.py +0 -0
  27. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/example_action_log.py +0 -0
  28. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/example_client.py +0 -0
  29. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/example_mcp_anthropic.py +0 -0
  30. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/example_mcp_openai.py +0 -0
  31. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/example_sync.py +0 -0
  32. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/example_task.py +0 -0
  33. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/example_tasks.py +0 -0
  34. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/example_verifier.py +0 -0
  35. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/export_tasks.py +0 -0
  36. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/gemini_example.py +0 -0
  37. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/import_tasks.py +0 -0
  38. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/json_tasks_example.py +0 -0
  39. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/nova_act_example.py +0 -0
  40. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/openai_example.py +0 -0
  41. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/openai_simple_example.py +0 -0
  42. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/query_builder_example.py +0 -0
  43. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/quickstart.py +0 -0
  44. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/test_cdp_logging.py +0 -0
  45. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/__init__.py +0 -0
  46. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/_async/__init__.py +0 -0
  47. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/_async/base.py +0 -0
  48. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/_async/env/__init__.py +0 -0
  49. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/_async/exceptions.py +0 -0
  50. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/_async/global_client.py +0 -0
  51. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/_async/instance/__init__.py +0 -0
  52. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/_async/instance/base.py +0 -0
  53. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/_async/models.py +0 -0
  54. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/_async/resources/__init__.py +0 -0
  55. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/_async/resources/base.py +0 -0
  56. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/_async/resources/browser.py +0 -0
  57. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/_async/resources/mcp.py +0 -0
  58. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/_async/verifiers/__init__.py +0 -0
  59. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/_async/verifiers/bundler.py +0 -0
  60. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/_async/verifiers/verifier.py +0 -0
  61. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/base.py +0 -0
  62. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/config.py +0 -0
  63. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/exceptions.py +0 -0
  64. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/global_client.py +0 -0
  65. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/instance/__init__.py +0 -0
  66. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/instance/base.py +0 -0
  67. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/instance/models.py +0 -0
  68. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/resources/__init__.py +0 -0
  69. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/resources/base.py +0 -0
  70. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/resources/browser.py +0 -0
  71. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/resources/mcp.py +0 -0
  72. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/types.py +0 -0
  73. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/verifiers/__init__.py +0 -0
  74. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/verifiers/bundler.py +0 -0
  75. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/verifiers/code.py +0 -0
  76. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/verifiers/db.py +0 -0
  77. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/verifiers/decorator.py +0 -0
  78. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/verifiers/parse.py +0 -0
  79. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/verifiers/sql_differ.py +0 -0
  80. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/verifiers/verifier.py +0 -0
  81. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet_python.egg-info/dependency_links.txt +0 -0
  82. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet_python.egg-info/requires.txt +0 -0
  83. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet_python.egg-info/top_level.txt +0 -0
  84. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/scripts/fix_sync_imports.py +0 -0
  85. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/scripts/unasync.py +0 -0
  86. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/setup.cfg +0 -0
  87. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/tests/__init__.py +0 -0
  88. {fleet_python-0.2.67 → fleet_python-0.2.69b2}/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.67
3
+ Version: 0.2.69b2
4
4
  Summary: Python SDK for Fleet environments
5
5
  Author-email: Fleet AI <nic@fleet.so>
6
6
  License: Apache-2.0
@@ -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 (
@@ -47,6 +47,11 @@ from .instance import (
47
47
  ResetResponse,
48
48
  ExecuteFunctionResponse,
49
49
  )
50
+ from ..instance.models import (
51
+ Resource as ResourceModel,
52
+ ResourceType,
53
+ ResourceMode,
54
+ )
50
55
  from ..config import (
51
56
  DEFAULT_MAX_RETRIES,
52
57
  DEFAULT_TIMEOUT,
@@ -212,6 +217,7 @@ class AsyncFleet:
212
217
  env_variables: Optional[Dict[str, Any]] = None,
213
218
  image_type: Optional[str] = None,
214
219
  ttl_seconds: Optional[int] = None,
220
+ run_id: Optional[str] = None,
215
221
  ) -> AsyncEnv:
216
222
  if ":" in env_key:
217
223
  env_key_part, env_version = env_key.split(":", 1)
@@ -247,6 +253,7 @@ class AsyncFleet:
247
253
  image_type=image_type,
248
254
  created_from="sdk",
249
255
  ttl_seconds=ttl_seconds,
256
+ run_id=run_id,
250
257
  )
251
258
 
252
259
  # Only use region-specific base URL if no custom base URL is set
@@ -269,13 +276,15 @@ class AsyncFleet:
269
276
  return await self.make(env_key=f"{task.env_id}:{task.version}")
270
277
 
271
278
  async def instances(
272
- self, status: Optional[str] = None, region: Optional[str] = None
279
+ self, status: Optional[str] = None, region: Optional[str] = None, run_id: Optional[str] = None
273
280
  ) -> List[AsyncEnv]:
274
281
  params = {}
275
282
  if status:
276
283
  params["status"] = status
277
284
  if region:
278
285
  params["region"] = region
286
+ if run_id:
287
+ params["run_id"] = run_id
279
288
 
280
289
  response = await self.client.request("GET", "/v1/env/instances", params=params)
281
290
  return [
@@ -283,11 +292,163 @@ class AsyncFleet:
283
292
  for instance_data in response.json()
284
293
  ]
285
294
 
286
- async def instance(self, instance_id: str) -> AsyncEnv:
287
- response = await self.client.request("GET", f"/v1/env/instances/{instance_id}")
288
- instance = AsyncEnv(client=self.client, **response.json())
289
- await instance.instance.load()
290
- return instance
295
+ async def instance(self, instance_id: Union[str, Dict[str, str]]) -> AsyncEnv:
296
+ """Create or connect to an environment instance.
297
+
298
+ Supports three modes based on input type:
299
+ 1. dict: Local filesystem mode - {"current": "./data.db", "seed": "./seed.db"}
300
+ 2. str starting with http:// or https://: Localhost/URL mode
301
+ 3. str (other): Remote cloud instance mode
302
+
303
+ Args:
304
+ instance_id: Instance identifier (str), URL (str starting with http://),
305
+ or local db mapping (dict)
306
+
307
+ Returns:
308
+ AsyncEnv: Environment instance
309
+ """
310
+ # Local filesystem mode - dict of resource names to file paths
311
+ if isinstance(instance_id, dict):
312
+ return self._create_local_instance(instance_id)
313
+
314
+ # Localhost/direct URL mode - string starting with http:// or https://
315
+ elif isinstance(instance_id, str) and instance_id.startswith(("http://", "https://")):
316
+ return self._create_url_instance(instance_id)
317
+
318
+ # Remote mode - existing behavior
319
+ else:
320
+ response = await self.client.request("GET", f"/v1/env/instances/{instance_id}")
321
+ instance = AsyncEnv(client=self.client, **response.json())
322
+ await instance.instance.load()
323
+ return instance
324
+
325
+ def _create_url_instance(self, base_url: str) -> AsyncEnv:
326
+ """Create instance connected to a direct URL (localhost or custom).
327
+
328
+ Args:
329
+ base_url: URL of the instance manager API
330
+
331
+ Returns:
332
+ AsyncEnv: Environment instance configured for URL mode
333
+ """
334
+ instance_client = AsyncInstanceClient(url=base_url, httpx_client=self._httpx_client)
335
+
336
+ # Create a minimal environment for URL mode
337
+ env = AsyncEnv(
338
+ client=self.client,
339
+ instance_id=base_url,
340
+ env_key="localhost",
341
+ version="",
342
+ status="running",
343
+ subdomain="localhost",
344
+ created_at="",
345
+ updated_at="",
346
+ terminated_at=None,
347
+ team_id="",
348
+ region="localhost",
349
+ env_variables=None,
350
+ data_key=None,
351
+ data_version=None,
352
+ urls=None,
353
+ health=None,
354
+ )
355
+ env._instance = instance_client
356
+ return env
357
+
358
+ @staticmethod
359
+ def _normalize_db_path(path: str) -> tuple[str, bool]:
360
+ """Normalize database path and detect if it's in-memory.
361
+
362
+ Args:
363
+ path: Database path - can be:
364
+ - File path: "./data.db"
365
+ - Plain memory: ":memory:"
366
+ - Named memory: ":memory:namespace"
367
+ - URI: "file:name?mode=memory&cache=shared"
368
+
369
+ Returns:
370
+ Tuple of (normalized_path, is_memory)
371
+ """
372
+ import uuid
373
+ import sqlite3
374
+
375
+ if path == ":memory:":
376
+ # Plain :memory: - create unique namespace
377
+ name = f"mem_{uuid.uuid4().hex[:8]}"
378
+ return f"file:{name}?mode=memory&cache=shared", True
379
+ elif path.startswith(":memory:"):
380
+ # Named memory: :memory:current -> file:current?mode=memory&cache=shared
381
+ namespace = path[8:] # Remove ":memory:" prefix
382
+ return f"file:{namespace}?mode=memory&cache=shared", True
383
+ elif "mode=memory" in path:
384
+ # Already a proper memory URI
385
+ return path, True
386
+ else:
387
+ # Regular file path
388
+ return path, False
389
+
390
+ def _create_local_instance(self, dbs: Dict[str, str]) -> AsyncEnv:
391
+ """Create instance with local file-based or in-memory SQLite resources.
392
+
393
+ Args:
394
+ dbs: Map of resource names to paths (e.g., {"current": "./data.db"} or
395
+ {"current": ":memory:current"})
396
+
397
+ Returns:
398
+ AsyncEnv: Environment instance configured for local mode
399
+ """
400
+ import sqlite3
401
+
402
+ instance_client = AsyncInstanceClient(url="local://", httpx_client=None)
403
+ instance_client._resources = [] # Mark as loaded
404
+ instance_client._memory_anchors = {} # Store anchor connections for in-memory DBs
405
+
406
+ # Store creation parameters for local AsyncSQLiteResources
407
+ # This allows db() to create new instances each time (matching HTTP mode behavior)
408
+ for name, path in dbs.items():
409
+ # Normalize path and detect if it's in-memory
410
+ normalized_path, is_memory = self._normalize_db_path(path)
411
+
412
+ # Create anchor connection for in-memory databases
413
+ # This keeps the database alive as long as the env exists
414
+ if is_memory:
415
+ anchor_conn = sqlite3.connect(normalized_path, uri=True)
416
+ instance_client._memory_anchors[name] = anchor_conn
417
+
418
+ resource_model = ResourceModel(
419
+ name=name,
420
+ type=ResourceType.db,
421
+ mode=ResourceMode.rw,
422
+ label=f"Local: {path}",
423
+ )
424
+ instance_client._resources_state[ResourceType.db.value][name] = {
425
+ 'type': 'local',
426
+ 'resource_model': resource_model,
427
+ 'db_path': normalized_path,
428
+ 'is_memory': is_memory
429
+ }
430
+
431
+ # Create a minimal environment for local mode
432
+ env = AsyncEnv(
433
+ client=self.client,
434
+ instance_id="local",
435
+ env_key="local",
436
+ version="",
437
+ status="running",
438
+ subdomain="local",
439
+ created_at="",
440
+ updated_at="",
441
+ terminated_at=None,
442
+ team_id="",
443
+ region="local",
444
+ env_variables=None,
445
+ data_key=None,
446
+ data_version=None,
447
+ urls=None,
448
+ health=None,
449
+ )
450
+ env._instance = instance_client
451
+ return env
291
452
 
292
453
  async def check_bundle_exists(self, bundle_hash: str) -> VerifiersCheckResponse:
293
454
  return await _check_bundle_exists(self.client, bundle_hash)
@@ -302,6 +463,28 @@ class AsyncFleet:
302
463
  async def delete(self, instance_id: str) -> InstanceResponse:
303
464
  return await _delete_instance(self.client, instance_id)
304
465
 
466
+ async def close(self, instance_id: str) -> InstanceResponse:
467
+ """Close (delete) a specific instance by ID.
468
+
469
+ Args:
470
+ instance_id: The instance ID to close
471
+
472
+ Returns:
473
+ InstanceResponse containing the deleted instance details
474
+ """
475
+ return await _delete_instance(self.client, instance_id)
476
+
477
+ async def close_all(self, run_id: str) -> List[InstanceResponse]:
478
+ """Close (delete) all instances associated with a run_id.
479
+
480
+ Args:
481
+ run_id: The run ID whose instances should be closed
482
+
483
+ Returns:
484
+ List[InstanceResponse] containing the deleted instances
485
+ """
486
+ return await _delete_instances_by_run_id(self.client, run_id)
487
+
305
488
  async def load_tasks_from_file(self, filename: str) -> List[Task]:
306
489
  with open(filename, "r", encoding="utf-8") as f:
307
490
  tasks_data = f.read()
@@ -811,6 +994,11 @@ async def _delete_instance(client: AsyncWrapper, instance_id: str) -> InstanceRe
811
994
  return InstanceResponse(**response.json())
812
995
 
813
996
 
997
+ async def _delete_instances_by_run_id(client: AsyncWrapper, run_id: str) -> List[InstanceResponse]:
998
+ response = await client.request("DELETE", f"/v1/env/instances/run/{run_id}")
999
+ return [InstanceResponse(**instance_data) for instance_data in response.json()]
1000
+
1001
+
814
1002
  async def _check_bundle_exists(
815
1003
  client: AsyncWrapper, bundle_hash: str
816
1004
  ) -> VerifiersCheckResponse:
@@ -1,5 +1,5 @@
1
1
  from ..client import AsyncFleet, AsyncEnv, Task
2
- from ...models import Environment as EnvironmentModel, AccountResponse
2
+ from ...models import Environment as EnvironmentModel, AccountResponse, InstanceResponse
3
3
  from typing import List, Optional, Dict, Any
4
4
 
5
5
 
@@ -10,6 +10,7 @@ async def make_async(
10
10
  env_variables: Optional[Dict[str, Any]] = None,
11
11
  image_type: Optional[str] = None,
12
12
  ttl_seconds: Optional[int] = None,
13
+ run_id: Optional[str] = None,
13
14
  ) -> AsyncEnv:
14
15
  return await AsyncFleet().make(
15
16
  env_key,
@@ -18,6 +19,7 @@ async def make_async(
18
19
  env_variables=env_variables,
19
20
  image_type=image_type,
20
21
  ttl_seconds=ttl_seconds,
22
+ run_id=run_id,
21
23
  )
22
24
 
23
25
 
@@ -34,14 +36,38 @@ async def list_regions_async() -> List[str]:
34
36
 
35
37
 
36
38
  async def list_instances_async(
37
- status: Optional[str] = None, region: Optional[str] = None
39
+ status: Optional[str] = None, region: Optional[str] = None, run_id: Optional[str] = None
38
40
  ) -> List[AsyncEnv]:
39
- return await AsyncFleet().instances(status=status, region=region)
41
+ return await AsyncFleet().instances(status=status, region=region, run_id=run_id)
40
42
 
41
43
 
42
44
  async def get_async(instance_id: str) -> AsyncEnv:
43
45
  return await AsyncFleet().instance(instance_id)
44
46
 
45
47
 
48
+ async def close_async(instance_id: str) -> InstanceResponse:
49
+ """Close (delete) a specific instance by ID.
50
+
51
+ Args:
52
+ instance_id: The instance ID to close
53
+
54
+ Returns:
55
+ InstanceResponse containing the deleted instance details
56
+ """
57
+ return await AsyncFleet().close(instance_id)
58
+
59
+
60
+ async def close_all_async(run_id: str) -> List[InstanceResponse]:
61
+ """Close (delete) all instances associated with a run_id.
62
+
63
+ Args:
64
+ run_id: The run ID whose instances should be closed
65
+
66
+ Returns:
67
+ List[InstanceResponse] containing the deleted instances
68
+ """
69
+ return await AsyncFleet().close_all(run_id)
70
+
71
+
46
72
  async def account_async() -> AccountResponse:
47
73
  return await AsyncFleet().account()
@@ -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)
@@ -214,13 +214,14 @@ class Task(BaseModel):
214
214
  region: Optional[str] = None,
215
215
  image_type: Optional[str] = None,
216
216
  ttl_seconds: Optional[int] = None,
217
+ run_id: Optional[str] = None,
217
218
  ):
218
219
  """Create an environment instance for this task's environment.
219
220
 
220
221
  Alias for make() method. Uses the task's env_id (and version if present) to create the env.
221
222
  """
222
223
  return await self.make(
223
- region=region, image_type=image_type, ttl_seconds=ttl_seconds
224
+ region=region, image_type=image_type, ttl_seconds=ttl_seconds, run_id=run_id
224
225
  )
225
226
 
226
227
  async def make(
@@ -228,6 +229,7 @@ class Task(BaseModel):
228
229
  region: Optional[str] = None,
229
230
  image_type: Optional[str] = None,
230
231
  ttl_seconds: Optional[int] = None,
232
+ run_id: Optional[str] = None,
231
233
  ):
232
234
  """Create an environment instance with task's configuration.
233
235
 
@@ -235,11 +237,13 @@ class Task(BaseModel):
235
237
  - env_key (env_id + version)
236
238
  - data_key (data_id + data_version, if present)
237
239
  - env_variables (if present)
240
+ - run_id (if present)
238
241
 
239
242
  Args:
240
243
  region: Optional AWS region for the environment
241
244
  image_type: Optional image type for the environment
242
245
  ttl_seconds: Optional TTL in seconds for the instance
246
+ run_id: Optional run ID to group instances
243
247
 
244
248
  Returns:
245
249
  Environment instance configured for this task
@@ -247,7 +251,7 @@ class Task(BaseModel):
247
251
  Example:
248
252
  task = fleet.Task(key="my-task", prompt="...", env_id="my-env",
249
253
  data_id="my-data", data_version="v1.0")
250
- env = await task.make(region="us-west-2")
254
+ env = await task.make(region="us-west-2", run_id="my-batch-123")
251
255
  """
252
256
  if not self.env_id:
253
257
  raise ValueError("Task has no env_id defined")
@@ -262,6 +266,7 @@ class Task(BaseModel):
262
266
  env_variables=self.env_variables if self.env_variables else None,
263
267
  image_type=image_type,
264
268
  ttl_seconds=ttl_seconds,
269
+ run_id=run_id,
265
270
  )
266
271
 
267
272
 
@@ -290,8 +295,11 @@ def verifier_from_string(
290
295
  # Remove lines like: @verifier(key="...")
291
296
  cleaned_code = re.sub(r"@verifier\([^)]*\)\s*\n", "", verifier_func)
292
297
  # Also remove the verifier import if present
293
- cleaned_code = re.sub(r"from fleet import.*verifier.*\n", "", cleaned_code)
294
- cleaned_code = re.sub(r"import.*verifier.*\n", "", cleaned_code)
298
+ # Use MULTILINE flag to match beginning of lines with ^
299
+ cleaned_code = re.sub(r"^from fleet\.verifiers.*import.*verifier.*$\n?", "", cleaned_code, flags=re.MULTILINE)
300
+ cleaned_code = re.sub(r"^from fleet import verifier.*$\n?", "", cleaned_code, flags=re.MULTILINE)
301
+ cleaned_code = re.sub(r"^import fleet\.verifiers.*$\n?", "", cleaned_code, flags=re.MULTILINE)
302
+ cleaned_code = re.sub(r"^import fleet$\n?", "", cleaned_code, flags=re.MULTILINE)
295
303
 
296
304
  # Create a local namespace for executing the code
297
305
  local_namespace = {