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.
- {fleet_python-0.2.67/fleet_python.egg-info → fleet_python-0.2.69b2}/PKG-INFO +1 -1
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/_async/client.py +195 -7
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/_async/env/client.py +29 -3
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/_async/instance/client.py +19 -4
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/_async/resources/sqlite.py +150 -1
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/_async/tasks.py +12 -4
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/client.py +195 -7
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/env/__init__.py +8 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/env/client.py +29 -3
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/instance/client.py +20 -5
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/models.py +2 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/resources/sqlite.py +143 -1
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/tasks.py +12 -4
- {fleet_python-0.2.67 → fleet_python-0.2.69b2/fleet_python.egg-info}/PKG-INFO +1 -1
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet_python.egg-info/SOURCES.txt +3 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/pyproject.toml +1 -1
- fleet_python-0.2.69b2/tests/test_instance_dispatch.py +607 -0
- fleet_python-0.2.69b2/tests/test_sqlite_resource_dual_mode.py +263 -0
- fleet_python-0.2.69b2/tests/test_sqlite_shared_memory_behavior.py +117 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/LICENSE +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/README.md +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/diff_example.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/dsl_example.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/example.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/exampleResume.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/example_account.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/example_action_log.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/example_client.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/example_mcp_anthropic.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/example_mcp_openai.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/example_sync.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/example_task.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/example_tasks.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/example_verifier.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/export_tasks.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/gemini_example.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/import_tasks.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/json_tasks_example.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/nova_act_example.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/openai_example.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/openai_simple_example.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/query_builder_example.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/quickstart.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/examples/test_cdp_logging.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/__init__.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/_async/__init__.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/_async/base.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/_async/env/__init__.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/_async/exceptions.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/_async/global_client.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/_async/instance/__init__.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/_async/instance/base.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/_async/models.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/_async/resources/__init__.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/_async/resources/base.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/_async/resources/browser.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/_async/resources/mcp.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/_async/verifiers/__init__.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/_async/verifiers/bundler.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/_async/verifiers/verifier.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/base.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/config.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/exceptions.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/global_client.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/instance/__init__.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/instance/base.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/instance/models.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/resources/__init__.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/resources/base.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/resources/browser.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/resources/mcp.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/types.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/verifiers/__init__.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/verifiers/bundler.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/verifiers/code.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/verifiers/db.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/verifiers/decorator.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/verifiers/parse.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/verifiers/sql_differ.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet/verifiers/verifier.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet_python.egg-info/dependency_links.txt +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet_python.egg-info/requires.txt +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/fleet_python.egg-info/top_level.txt +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/scripts/fix_sync_imports.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/scripts/unasync.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/setup.cfg +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/tests/__init__.py +0 -0
- {fleet_python-0.2.67 → fleet_python-0.2.69b2}/tests/test_verifier_from_string.py +0 -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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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__(
|
|
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
|
-
|
|
294
|
-
cleaned_code = re.sub(r"import.*verifier
|
|
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 = {
|