fleet-python 0.2.67__py3-none-any.whl → 0.2.69b2__py3-none-any.whl
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/_async/client.py +195 -7
- fleet/_async/env/client.py +29 -3
- fleet/_async/instance/client.py +19 -4
- fleet/_async/resources/sqlite.py +150 -1
- fleet/_async/tasks.py +12 -4
- fleet/client.py +195 -7
- fleet/env/__init__.py +8 -0
- fleet/env/client.py +29 -3
- fleet/instance/client.py +20 -5
- fleet/models.py +2 -0
- fleet/resources/sqlite.py +143 -1
- fleet/tasks.py +12 -4
- {fleet_python-0.2.67.dist-info → fleet_python-0.2.69b2.dist-info}/METADATA +1 -1
- {fleet_python-0.2.67.dist-info → fleet_python-0.2.69b2.dist-info}/RECORD +20 -17
- tests/test_instance_dispatch.py +607 -0
- tests/test_sqlite_resource_dual_mode.py +263 -0
- tests/test_sqlite_shared_memory_behavior.py +117 -0
- {fleet_python-0.2.67.dist-info → fleet_python-0.2.69b2.dist-info}/WHEEL +0 -0
- {fleet_python-0.2.67.dist-info → fleet_python-0.2.69b2.dist-info}/licenses/LICENSE +0 -0
- {fleet_python-0.2.67.dist-info → fleet_python-0.2.69b2.dist-info}/top_level.txt +0 -0
fleet/client.py
CHANGED
|
@@ -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, SyncWrapper
|
|
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 Fleet:
|
|
|
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
|
) -> SyncEnv:
|
|
216
222
|
if ":" in env_key:
|
|
217
223
|
env_key_part, env_version = env_key.split(":", 1)
|
|
@@ -247,6 +253,7 @@ class Fleet:
|
|
|
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 Fleet:
|
|
|
269
276
|
return self.make(env_key=f"{task.env_id}:{task.version}")
|
|
270
277
|
|
|
271
278
|
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[SyncEnv]:
|
|
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 = self.client.request("GET", "/v1/env/instances", params=params)
|
|
281
290
|
return [
|
|
@@ -283,11 +292,163 @@ class Fleet:
|
|
|
283
292
|
for instance_data in response.json()
|
|
284
293
|
]
|
|
285
294
|
|
|
286
|
-
def instance(self, instance_id: str) -> SyncEnv:
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
295
|
+
def instance(self, instance_id: Union[str, Dict[str, str]]) -> SyncEnv:
|
|
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
|
+
SyncEnv: 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 = self.client.request("GET", f"/v1/env/instances/{instance_id}")
|
|
321
|
+
instance = SyncEnv(client=self.client, **response.json())
|
|
322
|
+
instance.instance.load()
|
|
323
|
+
return instance
|
|
324
|
+
|
|
325
|
+
def _create_url_instance(self, base_url: str) -> SyncEnv:
|
|
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
|
+
SyncEnv: Environment instance configured for URL mode
|
|
333
|
+
"""
|
|
334
|
+
instance_client = InstanceClient(url=base_url, httpx_client=self._httpx_client)
|
|
335
|
+
|
|
336
|
+
# Create a minimal environment for URL mode
|
|
337
|
+
env = SyncEnv(
|
|
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]) -> SyncEnv:
|
|
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
|
+
SyncEnv: Environment instance configured for local mode
|
|
399
|
+
"""
|
|
400
|
+
import sqlite3
|
|
401
|
+
|
|
402
|
+
instance_client = InstanceClient(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 SQLiteResources
|
|
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 = SyncEnv(
|
|
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
|
def check_bundle_exists(self, bundle_hash: str) -> VerifiersCheckResponse:
|
|
293
454
|
return _check_bundle_exists(self.client, bundle_hash)
|
|
@@ -300,6 +461,28 @@ class Fleet:
|
|
|
300
461
|
def delete(self, instance_id: str) -> InstanceResponse:
|
|
301
462
|
return _delete_instance(self.client, instance_id)
|
|
302
463
|
|
|
464
|
+
def close(self, instance_id: str) -> InstanceResponse:
|
|
465
|
+
"""Close (delete) a specific instance by ID.
|
|
466
|
+
|
|
467
|
+
Args:
|
|
468
|
+
instance_id: The instance ID to close
|
|
469
|
+
|
|
470
|
+
Returns:
|
|
471
|
+
InstanceResponse containing the deleted instance details
|
|
472
|
+
"""
|
|
473
|
+
return _delete_instance(self.client, instance_id)
|
|
474
|
+
|
|
475
|
+
def close_all(self, run_id: str) -> List[InstanceResponse]:
|
|
476
|
+
"""Close (delete) all instances associated with a run_id.
|
|
477
|
+
|
|
478
|
+
Args:
|
|
479
|
+
run_id: The run ID whose instances should be closed
|
|
480
|
+
|
|
481
|
+
Returns:
|
|
482
|
+
List[InstanceResponse] containing the deleted instances
|
|
483
|
+
"""
|
|
484
|
+
return _delete_instances_by_run_id(self.client, run_id)
|
|
485
|
+
|
|
303
486
|
def load_tasks_from_file(self, filename: str) -> List[Task]:
|
|
304
487
|
with open(filename, "r", encoding="utf-8") as f:
|
|
305
488
|
tasks_data = f.read()
|
|
@@ -810,6 +993,11 @@ def _delete_instance(client: SyncWrapper, instance_id: str) -> InstanceResponse:
|
|
|
810
993
|
return InstanceResponse(**response.json())
|
|
811
994
|
|
|
812
995
|
|
|
996
|
+
def _delete_instances_by_run_id(client: SyncWrapper, run_id: str) -> List[InstanceResponse]:
|
|
997
|
+
response = client.request("DELETE", f"/v1/env/instances/run/{run_id}")
|
|
998
|
+
return [InstanceResponse(**instance_data) for instance_data in response.json()]
|
|
999
|
+
|
|
1000
|
+
|
|
813
1001
|
def _check_bundle_exists(
|
|
814
1002
|
client: SyncWrapper, bundle_hash: str
|
|
815
1003
|
) -> VerifiersCheckResponse:
|
fleet/env/__init__.py
CHANGED
|
@@ -7,6 +7,8 @@ from .client import (
|
|
|
7
7
|
list_regions,
|
|
8
8
|
get,
|
|
9
9
|
list_instances,
|
|
10
|
+
close,
|
|
11
|
+
close_all,
|
|
10
12
|
account,
|
|
11
13
|
)
|
|
12
14
|
|
|
@@ -17,6 +19,8 @@ from .._async.env.client import (
|
|
|
17
19
|
list_regions_async,
|
|
18
20
|
get_async,
|
|
19
21
|
list_instances_async,
|
|
22
|
+
close_async,
|
|
23
|
+
close_all_async,
|
|
20
24
|
account_async,
|
|
21
25
|
)
|
|
22
26
|
|
|
@@ -27,11 +31,15 @@ __all__ = [
|
|
|
27
31
|
"list_regions",
|
|
28
32
|
"list_instances",
|
|
29
33
|
"get",
|
|
34
|
+
"close",
|
|
35
|
+
"close_all",
|
|
30
36
|
"make_async",
|
|
31
37
|
"list_envs_async",
|
|
32
38
|
"list_regions_async",
|
|
33
39
|
"list_instances_async",
|
|
34
40
|
"get_async",
|
|
41
|
+
"close_async",
|
|
42
|
+
"close_all_async",
|
|
35
43
|
"account",
|
|
36
44
|
"account_async",
|
|
37
45
|
]
|
fleet/env/client.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from ..client import Fleet, SyncEnv, 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 @@ def make(
|
|
|
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
|
) -> SyncEnv:
|
|
14
15
|
return Fleet().make(
|
|
15
16
|
env_key,
|
|
@@ -18,6 +19,7 @@ def make(
|
|
|
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 @@ def list_regions() -> List[str]:
|
|
|
34
36
|
|
|
35
37
|
|
|
36
38
|
def list_instances(
|
|
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[SyncEnv]:
|
|
39
|
-
return Fleet().instances(status=status, region=region)
|
|
41
|
+
return Fleet().instances(status=status, region=region, run_id=run_id)
|
|
40
42
|
|
|
41
43
|
|
|
42
44
|
def get(instance_id: str) -> SyncEnv:
|
|
43
45
|
return Fleet().instance(instance_id)
|
|
44
46
|
|
|
45
47
|
|
|
48
|
+
def close(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 Fleet().close(instance_id)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def close_all(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 Fleet().close_all(run_id)
|
|
70
|
+
|
|
71
|
+
|
|
46
72
|
def account() -> AccountResponse:
|
|
47
73
|
return Fleet().account()
|
fleet/instance/client.py
CHANGED
|
@@ -83,9 +83,17 @@ class InstanceClient:
|
|
|
83
83
|
Returns:
|
|
84
84
|
An SQLite database resource for the given database name
|
|
85
85
|
"""
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
)
|
|
86
|
+
resource_info = self._resources_state[ResourceType.db.value][name]
|
|
87
|
+
# Local mode - resource_info is a dict with creation parameters
|
|
88
|
+
if isinstance(resource_info, dict) and resource_info.get('type') == 'local':
|
|
89
|
+
# Create new instance each time (matching HTTP mode behavior)
|
|
90
|
+
return SQLiteResource(
|
|
91
|
+
resource_info['resource_model'],
|
|
92
|
+
client=None,
|
|
93
|
+
db_path=resource_info['db_path']
|
|
94
|
+
)
|
|
95
|
+
# HTTP mode - resource_info is a ResourceModel, create new wrapper
|
|
96
|
+
return SQLiteResource(resource_info, self.client)
|
|
89
97
|
|
|
90
98
|
def browser(self, name: str) -> BrowserResource:
|
|
91
99
|
return BrowserResource(
|
|
@@ -175,10 +183,17 @@ class InstanceClient:
|
|
|
175
183
|
response = self.client.request("GET", "/health")
|
|
176
184
|
return HealthResponse(**response.json())
|
|
177
185
|
|
|
186
|
+
def close(self):
|
|
187
|
+
"""Close anchor connections for in-memory databases."""
|
|
188
|
+
if hasattr(self, '_memory_anchors'):
|
|
189
|
+
for conn in self._memory_anchors.values():
|
|
190
|
+
conn.close()
|
|
191
|
+
self._memory_anchors.clear()
|
|
192
|
+
|
|
178
193
|
def __enter__(self):
|
|
179
|
-
"""
|
|
194
|
+
"""Context manager entry."""
|
|
180
195
|
return self
|
|
181
196
|
|
|
182
197
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
183
|
-
"""
|
|
198
|
+
"""Context manager exit."""
|
|
184
199
|
self.close()
|
fleet/models.py
CHANGED
|
@@ -51,6 +51,7 @@ class Instance(BaseModel):
|
|
|
51
51
|
team_id: str = Field(..., title="Team Id")
|
|
52
52
|
region: str = Field(..., title="Region")
|
|
53
53
|
env_variables: Optional[Dict[str, Any]] = Field(None, title="Env Variables")
|
|
54
|
+
run_id: Optional[str] = Field(None, title="Run Id")
|
|
54
55
|
|
|
55
56
|
|
|
56
57
|
class InstanceRequest(BaseModel):
|
|
@@ -363,6 +364,7 @@ class InstanceResponse(BaseModel):
|
|
|
363
364
|
data_version: Optional[str] = Field(None, title="Data Version")
|
|
364
365
|
urls: Optional[InstanceURLs] = Field(None, title="Urls")
|
|
365
366
|
health: Optional[bool] = Field(None, title="Health")
|
|
367
|
+
run_id: Optional[str] = Field(None, title="Run Id")
|
|
366
368
|
|
|
367
369
|
|
|
368
370
|
class AccountResponse(BaseModel):
|
fleet/resources/sqlite.py
CHANGED
|
@@ -675,17 +675,97 @@ class SyncQueryBuilder:
|
|
|
675
675
|
|
|
676
676
|
|
|
677
677
|
class SQLiteResource(Resource):
|
|
678
|
-
def __init__(
|
|
678
|
+
def __init__(
|
|
679
|
+
self,
|
|
680
|
+
resource: ResourceModel,
|
|
681
|
+
client: Optional["SyncWrapper"] = None,
|
|
682
|
+
db_path: Optional[str] = None,
|
|
683
|
+
):
|
|
679
684
|
super().__init__(resource)
|
|
680
685
|
self.client = client
|
|
686
|
+
self.db_path = db_path
|
|
687
|
+
self._mode = "direct" if db_path else "http"
|
|
688
|
+
|
|
689
|
+
@property
|
|
690
|
+
def mode(self) -> str:
|
|
691
|
+
"""Return the mode of this resource: 'direct' (local file) or 'http' (remote API)."""
|
|
692
|
+
return self._mode
|
|
681
693
|
|
|
682
694
|
def describe(self) -> DescribeResponse:
|
|
683
695
|
"""Describe the SQLite database schema."""
|
|
696
|
+
if self._mode == "direct":
|
|
697
|
+
return self._describe_direct()
|
|
698
|
+
else:
|
|
699
|
+
return self._describe_http()
|
|
700
|
+
|
|
701
|
+
def _describe_http(self) -> DescribeResponse:
|
|
702
|
+
"""Describe database schema via HTTP API."""
|
|
684
703
|
response = self.client.request(
|
|
685
704
|
"GET", f"/resources/sqlite/{self.resource.name}/describe"
|
|
686
705
|
)
|
|
687
706
|
return DescribeResponse(**response.json())
|
|
688
707
|
|
|
708
|
+
def _describe_direct(self) -> DescribeResponse:
|
|
709
|
+
"""Describe database schema from local file or in-memory database."""
|
|
710
|
+
try:
|
|
711
|
+
# Check if we need URI mode (for shared memory databases)
|
|
712
|
+
use_uri = 'mode=memory' in self.db_path
|
|
713
|
+
conn = sqlite3.connect(self.db_path, uri=use_uri)
|
|
714
|
+
cursor = conn.cursor()
|
|
715
|
+
|
|
716
|
+
# Get all tables
|
|
717
|
+
cursor.execute(
|
|
718
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
|
|
719
|
+
)
|
|
720
|
+
table_names = [row[0] for row in cursor.fetchall()]
|
|
721
|
+
|
|
722
|
+
tables = []
|
|
723
|
+
for table_name in table_names:
|
|
724
|
+
# Get table info
|
|
725
|
+
cursor.execute(f"PRAGMA table_info({table_name})")
|
|
726
|
+
columns = cursor.fetchall()
|
|
727
|
+
|
|
728
|
+
# Get CREATE TABLE SQL
|
|
729
|
+
cursor.execute(
|
|
730
|
+
f"SELECT sql FROM sqlite_master WHERE type='table' AND name=?",
|
|
731
|
+
(table_name,)
|
|
732
|
+
)
|
|
733
|
+
sql_row = cursor.fetchone()
|
|
734
|
+
create_sql = sql_row[0] if sql_row else ""
|
|
735
|
+
|
|
736
|
+
table_schema = {
|
|
737
|
+
"name": table_name,
|
|
738
|
+
"sql": create_sql,
|
|
739
|
+
"columns": [
|
|
740
|
+
{
|
|
741
|
+
"name": col[1],
|
|
742
|
+
"type": col[2],
|
|
743
|
+
"notnull": bool(col[3]),
|
|
744
|
+
"default_value": col[4],
|
|
745
|
+
"primary_key": col[5] > 0,
|
|
746
|
+
}
|
|
747
|
+
for col in columns
|
|
748
|
+
],
|
|
749
|
+
}
|
|
750
|
+
tables.append(table_schema)
|
|
751
|
+
|
|
752
|
+
conn.close()
|
|
753
|
+
|
|
754
|
+
return DescribeResponse(
|
|
755
|
+
success=True,
|
|
756
|
+
resource_name=self.resource.name,
|
|
757
|
+
tables=tables,
|
|
758
|
+
message="Schema retrieved from local file",
|
|
759
|
+
)
|
|
760
|
+
except Exception as e:
|
|
761
|
+
return DescribeResponse(
|
|
762
|
+
success=False,
|
|
763
|
+
resource_name=self.resource.name,
|
|
764
|
+
tables=None,
|
|
765
|
+
error=str(e),
|
|
766
|
+
message=f"Failed to describe database: {str(e)}",
|
|
767
|
+
)
|
|
768
|
+
|
|
689
769
|
def query(self, query: str, args: Optional[List[Any]] = None) -> QueryResponse:
|
|
690
770
|
return self._query(query, args, read_only=True)
|
|
691
771
|
|
|
@@ -695,6 +775,15 @@ class SQLiteResource(Resource):
|
|
|
695
775
|
def _query(
|
|
696
776
|
self, query: str, args: Optional[List[Any]] = None, read_only: bool = True
|
|
697
777
|
) -> QueryResponse:
|
|
778
|
+
if self._mode == "direct":
|
|
779
|
+
return self._query_direct(query, args, read_only)
|
|
780
|
+
else:
|
|
781
|
+
return self._query_http(query, args, read_only)
|
|
782
|
+
|
|
783
|
+
def _query_http(
|
|
784
|
+
self, query: str, args: Optional[List[Any]] = None, read_only: bool = True
|
|
785
|
+
) -> QueryResponse:
|
|
786
|
+
"""Execute query via HTTP API."""
|
|
698
787
|
request = QueryRequest(query=query, args=args, read_only=read_only)
|
|
699
788
|
response = self.client.request(
|
|
700
789
|
"POST",
|
|
@@ -703,6 +792,59 @@ class SQLiteResource(Resource):
|
|
|
703
792
|
)
|
|
704
793
|
return QueryResponse(**response.json())
|
|
705
794
|
|
|
795
|
+
def _query_direct(
|
|
796
|
+
self, query: str, args: Optional[List[Any]] = None, read_only: bool = True
|
|
797
|
+
) -> QueryResponse:
|
|
798
|
+
"""Execute query directly on local SQLite file or in-memory database."""
|
|
799
|
+
try:
|
|
800
|
+
# Check if we need URI mode (for shared memory databases)
|
|
801
|
+
use_uri = 'mode=memory' in self.db_path
|
|
802
|
+
conn = sqlite3.connect(self.db_path, uri=use_uri)
|
|
803
|
+
cursor = conn.cursor()
|
|
804
|
+
|
|
805
|
+
# Execute the query
|
|
806
|
+
if args:
|
|
807
|
+
cursor.execute(query, args)
|
|
808
|
+
else:
|
|
809
|
+
cursor.execute(query)
|
|
810
|
+
|
|
811
|
+
# For write operations, commit the transaction
|
|
812
|
+
if not read_only:
|
|
813
|
+
conn.commit()
|
|
814
|
+
|
|
815
|
+
# Get column names if available
|
|
816
|
+
columns = [desc[0] for desc in cursor.description] if cursor.description else []
|
|
817
|
+
|
|
818
|
+
# Fetch results for SELECT queries
|
|
819
|
+
rows = []
|
|
820
|
+
rows_affected = 0
|
|
821
|
+
last_insert_id = None
|
|
822
|
+
|
|
823
|
+
if cursor.description: # SELECT query
|
|
824
|
+
rows = cursor.fetchall()
|
|
825
|
+
else: # INSERT/UPDATE/DELETE
|
|
826
|
+
rows_affected = cursor.rowcount
|
|
827
|
+
last_insert_id = cursor.lastrowid if cursor.lastrowid else None
|
|
828
|
+
|
|
829
|
+
conn.close()
|
|
830
|
+
|
|
831
|
+
return QueryResponse(
|
|
832
|
+
success=True,
|
|
833
|
+
columns=columns if columns else None,
|
|
834
|
+
rows=rows if rows else None,
|
|
835
|
+
rows_affected=rows_affected if rows_affected > 0 else None,
|
|
836
|
+
last_insert_id=last_insert_id,
|
|
837
|
+
message="Query executed successfully",
|
|
838
|
+
)
|
|
839
|
+
except Exception as e:
|
|
840
|
+
return QueryResponse(
|
|
841
|
+
success=False,
|
|
842
|
+
columns=None,
|
|
843
|
+
rows=None,
|
|
844
|
+
error=str(e),
|
|
845
|
+
message=f"Query failed: {str(e)}",
|
|
846
|
+
)
|
|
847
|
+
|
|
706
848
|
def table(self, table_name: str) -> SyncQueryBuilder:
|
|
707
849
|
"""Create a query builder for the specified table."""
|
|
708
850
|
return SyncQueryBuilder(self, table_name)
|
fleet/tasks.py
CHANGED
|
@@ -207,18 +207,20 @@ class Task(BaseModel):
|
|
|
207
207
|
region: Optional[str] = None,
|
|
208
208
|
image_type: Optional[str] = None,
|
|
209
209
|
ttl_seconds: Optional[int] = None,
|
|
210
|
+
run_id: Optional[str] = None,
|
|
210
211
|
):
|
|
211
212
|
"""Create an environment instance for this task's environment.
|
|
212
213
|
|
|
213
214
|
Alias for make() method. Uses the task's env_id (and version if present) to create the env.
|
|
214
215
|
"""
|
|
215
|
-
return self.make(region=region, image_type=image_type, ttl_seconds=ttl_seconds)
|
|
216
|
+
return self.make(region=region, image_type=image_type, ttl_seconds=ttl_seconds, run_id=run_id)
|
|
216
217
|
|
|
217
218
|
def make(
|
|
218
219
|
self,
|
|
219
220
|
region: Optional[str] = None,
|
|
220
221
|
image_type: Optional[str] = None,
|
|
221
222
|
ttl_seconds: Optional[int] = None,
|
|
223
|
+
run_id: Optional[str] = None,
|
|
222
224
|
):
|
|
223
225
|
"""Create an environment instance with task's configuration.
|
|
224
226
|
|
|
@@ -226,11 +228,13 @@ class Task(BaseModel):
|
|
|
226
228
|
- env_key (env_id + version)
|
|
227
229
|
- data_key (data_id + data_version, if present)
|
|
228
230
|
- env_variables (if present)
|
|
231
|
+
- run_id (if present)
|
|
229
232
|
|
|
230
233
|
Args:
|
|
231
234
|
region: Optional AWS region for the environment
|
|
232
235
|
image_type: Optional image type for the environment
|
|
233
236
|
ttl_seconds: Optional TTL in seconds for the instance
|
|
237
|
+
run_id: Optional run ID to group instances
|
|
234
238
|
|
|
235
239
|
Returns:
|
|
236
240
|
Environment instance configured for this task
|
|
@@ -238,7 +242,7 @@ class Task(BaseModel):
|
|
|
238
242
|
Example:
|
|
239
243
|
task = fleet.Task(key="my-task", prompt="...", env_id="my-env",
|
|
240
244
|
data_id="my-data", data_version="v1.0")
|
|
241
|
-
env = task.make(region="us-west-2")
|
|
245
|
+
env = task.make(region="us-west-2", run_id="my-batch-123")
|
|
242
246
|
"""
|
|
243
247
|
if not self.env_id:
|
|
244
248
|
raise ValueError("Task has no env_id defined")
|
|
@@ -253,6 +257,7 @@ class Task(BaseModel):
|
|
|
253
257
|
env_variables=self.env_variables if self.env_variables else None,
|
|
254
258
|
image_type=image_type,
|
|
255
259
|
ttl_seconds=ttl_seconds,
|
|
260
|
+
run_id=run_id,
|
|
256
261
|
)
|
|
257
262
|
|
|
258
263
|
|
|
@@ -281,8 +286,11 @@ def verifier_from_string(
|
|
|
281
286
|
# Remove lines like: @verifier(key="...")
|
|
282
287
|
cleaned_code = re.sub(r"@verifier\([^)]*\)\s*\n", "", verifier_func)
|
|
283
288
|
# Also remove the verifier import if present
|
|
284
|
-
|
|
285
|
-
cleaned_code = re.sub(r"import.*verifier
|
|
289
|
+
# Use MULTILINE flag to match beginning of lines with ^
|
|
290
|
+
cleaned_code = re.sub(r"^from fleet\.verifiers.*import.*verifier.*$\n?", "", cleaned_code, flags=re.MULTILINE)
|
|
291
|
+
cleaned_code = re.sub(r"^from fleet import verifier.*$\n?", "", cleaned_code, flags=re.MULTILINE)
|
|
292
|
+
cleaned_code = re.sub(r"^import fleet\.verifiers.*$\n?", "", cleaned_code, flags=re.MULTILINE)
|
|
293
|
+
cleaned_code = re.sub(r"^import fleet$\n?", "", cleaned_code, flags=re.MULTILINE)
|
|
286
294
|
|
|
287
295
|
# Create a globals namespace with all required imports
|
|
288
296
|
exec_globals = globals().copy()
|