fleet-python 0.2.66b4__tar.gz → 0.2.68__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 (87) hide show
  1. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/PKG-INFO +1 -1
  2. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/examples/export_tasks.py +11 -1
  3. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/examples/import_tasks.py +15 -0
  4. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/_async/client.py +36 -3
  5. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/_async/env/client.py +29 -3
  6. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/_async/models.py +3 -0
  7. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/_async/tasks.py +44 -26
  8. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/client.py +36 -3
  9. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/env/__init__.py +8 -0
  10. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/env/client.py +29 -3
  11. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/models.py +5 -0
  12. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/tasks.py +39 -25
  13. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet_python.egg-info/PKG-INFO +1 -1
  14. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet_python.egg-info/SOURCES.txt +1 -3
  15. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/pyproject.toml +1 -1
  16. fleet_python-0.2.66b4/fleet/verifiers/parsing.py +0 -106
  17. fleet_python-0.2.66b4/tests/test_verifier_security.py +0 -427
  18. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/LICENSE +0 -0
  19. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/README.md +0 -0
  20. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/examples/diff_example.py +0 -0
  21. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/examples/dsl_example.py +0 -0
  22. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/examples/example.py +0 -0
  23. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/examples/exampleResume.py +0 -0
  24. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/examples/example_account.py +0 -0
  25. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/examples/example_action_log.py +0 -0
  26. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/examples/example_client.py +0 -0
  27. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/examples/example_mcp_anthropic.py +0 -0
  28. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/examples/example_mcp_openai.py +0 -0
  29. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/examples/example_sync.py +0 -0
  30. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/examples/example_task.py +0 -0
  31. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/examples/example_tasks.py +0 -0
  32. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/examples/example_verifier.py +0 -0
  33. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/examples/gemini_example.py +0 -0
  34. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/examples/json_tasks_example.py +0 -0
  35. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/examples/nova_act_example.py +0 -0
  36. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/examples/openai_example.py +0 -0
  37. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/examples/openai_simple_example.py +0 -0
  38. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/examples/query_builder_example.py +0 -0
  39. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/examples/quickstart.py +0 -0
  40. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/examples/test_cdp_logging.py +0 -0
  41. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/__init__.py +0 -0
  42. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/_async/__init__.py +0 -0
  43. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/_async/base.py +0 -0
  44. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/_async/env/__init__.py +0 -0
  45. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/_async/exceptions.py +0 -0
  46. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/_async/global_client.py +0 -0
  47. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/_async/instance/__init__.py +0 -0
  48. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/_async/instance/base.py +0 -0
  49. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/_async/instance/client.py +0 -0
  50. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/_async/resources/__init__.py +0 -0
  51. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/_async/resources/base.py +0 -0
  52. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/_async/resources/browser.py +0 -0
  53. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/_async/resources/mcp.py +0 -0
  54. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/_async/resources/sqlite.py +0 -0
  55. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/_async/verifiers/__init__.py +0 -0
  56. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/_async/verifiers/bundler.py +0 -0
  57. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/_async/verifiers/verifier.py +0 -0
  58. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/base.py +0 -0
  59. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/config.py +0 -0
  60. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/exceptions.py +0 -0
  61. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/global_client.py +0 -0
  62. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/instance/__init__.py +0 -0
  63. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/instance/base.py +0 -0
  64. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/instance/client.py +0 -0
  65. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/instance/models.py +0 -0
  66. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/resources/__init__.py +0 -0
  67. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/resources/base.py +0 -0
  68. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/resources/browser.py +0 -0
  69. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/resources/mcp.py +0 -0
  70. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/resources/sqlite.py +0 -0
  71. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/types.py +0 -0
  72. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/verifiers/__init__.py +0 -0
  73. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/verifiers/bundler.py +0 -0
  74. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/verifiers/code.py +0 -0
  75. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/verifiers/db.py +0 -0
  76. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/verifiers/decorator.py +0 -0
  77. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/verifiers/parse.py +0 -0
  78. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/verifiers/sql_differ.py +0 -0
  79. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet/verifiers/verifier.py +0 -0
  80. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet_python.egg-info/dependency_links.txt +0 -0
  81. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet_python.egg-info/requires.txt +0 -0
  82. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/fleet_python.egg-info/top_level.txt +0 -0
  83. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/scripts/fix_sync_imports.py +0 -0
  84. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/scripts/unasync.py +0 -0
  85. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/setup.cfg +0 -0
  86. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/tests/__init__.py +0 -0
  87. {fleet_python-0.2.66b4 → fleet_python-0.2.68}/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.66b4
3
+ Version: 0.2.68
4
4
  Summary: Python SDK for Fleet environments
5
5
  Author-email: Fleet AI <nic@fleet.so>
6
6
  License: Apache-2.0
@@ -27,6 +27,12 @@ def main():
27
27
  help="Optional task project key to filter tasks",
28
28
  default=None,
29
29
  )
30
+ parser.add_argument(
31
+ "--env-key",
32
+ "-e",
33
+ help="Optional environment key to filter tasks",
34
+ default=None,
35
+ )
30
36
  parser.add_argument(
31
37
  "--output",
32
38
  "-o",
@@ -42,12 +48,13 @@ def main():
42
48
  args.project_key is not None,
43
49
  args.task_keys is not None,
44
50
  args.task_project_key is not None,
51
+ args.env_key is not None,
45
52
  ]
46
53
  )
47
54
 
48
55
  if filters_specified > 1:
49
56
  parser.error(
50
- "Cannot specify multiple filters. Use only one of --project-key, --task-keys, or --task-project-key."
57
+ "Cannot specify multiple filters. Use only one of --project-key, --task-keys, --task-project-key, or --env-key."
51
58
  )
52
59
 
53
60
  # Get account info
@@ -66,6 +73,9 @@ def main():
66
73
  elif args.task_project_key:
67
74
  print(f"Loading tasks from task project: {args.task_project_key}")
68
75
  tasks = fleet.load_tasks(task_project_key=args.task_project_key)
76
+ elif args.env_key:
77
+ print(f"Loading tasks from environment: {args.env_key}")
78
+ tasks = fleet.load_tasks(env_key=args.env_key)
69
79
  else:
70
80
  print("Loading all tasks")
71
81
  tasks = fleet.load_tasks()
@@ -210,9 +210,18 @@ async def main():
210
210
  action="store_true",
211
211
  help="Skip the verifier sanity check (not recommended)",
212
212
  )
213
+ parser.add_argument(
214
+ "--sanity-check-only",
215
+ action="store_true",
216
+ help="Only run the sanity check without importing tasks",
217
+ )
213
218
 
214
219
  args = parser.parse_args()
215
220
 
221
+ # Validate conflicting flags
222
+ if args.skip_sanity_check and args.sanity_check_only:
223
+ parser.error("Cannot use --skip-sanity-check and --sanity-check-only together")
224
+
216
225
  # Load and parse the JSON file
217
226
  try:
218
227
  with open(args.json_file, "r", encoding="utf-8") as f:
@@ -286,6 +295,12 @@ async def main():
286
295
  success, errors = await run_verifier_sanity_check(tasks, client)
287
296
  if not success:
288
297
  sys.exit(1)
298
+
299
+ # If only doing sanity check, exit successfully here
300
+ if args.sanity_check_only:
301
+ print("\n✓ Sanity check complete! (--sanity-check-only)")
302
+ print("Tasks are ready to import.")
303
+ sys.exit(0)
289
304
  else:
290
305
  print("\n⚠️ Skipping sanity check (--skip-sanity-check)")
291
306
 
@@ -212,6 +212,7 @@ class AsyncFleet:
212
212
  env_variables: Optional[Dict[str, Any]] = None,
213
213
  image_type: Optional[str] = None,
214
214
  ttl_seconds: Optional[int] = None,
215
+ run_id: Optional[str] = None,
215
216
  ) -> AsyncEnv:
216
217
  if ":" in env_key:
217
218
  env_key_part, env_version = env_key.split(":", 1)
@@ -247,6 +248,7 @@ class AsyncFleet:
247
248
  image_type=image_type,
248
249
  created_from="sdk",
249
250
  ttl_seconds=ttl_seconds,
251
+ run_id=run_id,
250
252
  )
251
253
 
252
254
  # Only use region-specific base URL if no custom base URL is set
@@ -269,13 +271,15 @@ class AsyncFleet:
269
271
  return await self.make(env_key=f"{task.env_id}:{task.version}")
270
272
 
271
273
  async def instances(
272
- self, status: Optional[str] = None, region: Optional[str] = None
274
+ self, status: Optional[str] = None, region: Optional[str] = None, run_id: Optional[str] = None
273
275
  ) -> List[AsyncEnv]:
274
276
  params = {}
275
277
  if status:
276
278
  params["status"] = status
277
279
  if region:
278
280
  params["region"] = region
281
+ if run_id:
282
+ params["run_id"] = run_id
279
283
 
280
284
  response = await self.client.request("GET", "/v1/env/instances", params=params)
281
285
  return [
@@ -302,6 +306,28 @@ class AsyncFleet:
302
306
  async def delete(self, instance_id: str) -> InstanceResponse:
303
307
  return await _delete_instance(self.client, instance_id)
304
308
 
309
+ async def close(self, instance_id: str) -> InstanceResponse:
310
+ """Close (delete) a specific instance by ID.
311
+
312
+ Args:
313
+ instance_id: The instance ID to close
314
+
315
+ Returns:
316
+ InstanceResponse containing the deleted instance details
317
+ """
318
+ return await _delete_instance(self.client, instance_id)
319
+
320
+ async def close_all(self, run_id: str) -> List[InstanceResponse]:
321
+ """Close (delete) all instances associated with a run_id.
322
+
323
+ Args:
324
+ run_id: The run ID whose instances should be closed
325
+
326
+ Returns:
327
+ List[InstanceResponse] containing the deleted instances
328
+ """
329
+ return await _delete_instances_by_run_id(self.client, run_id)
330
+
305
331
  async def load_tasks_from_file(self, filename: str) -> List[Task]:
306
332
  with open(filename, "r", encoding="utf-8") as f:
307
333
  tasks_data = f.read()
@@ -553,7 +579,7 @@ class AsyncFleet:
553
579
  env_variables=task_response.env_variables or {},
554
580
  verifier_func=verifier_func, # Set verifier code
555
581
  verifier=verifier, # Use created verifier or None
556
- metadata={}, # Default empty metadata
582
+ metadata=task_response.metadata or {},
557
583
  output_json_schema=getattr(task_response, "output_json_schema", None), # Get output_json_schema if available
558
584
  )
559
585
  tasks.append(task)
@@ -708,6 +734,7 @@ class AsyncFleet:
708
734
  task_key: str,
709
735
  prompt: Optional[str] = None,
710
736
  verifier_code: Optional[str] = None,
737
+ metadata: Optional[Dict[str, Any]] = None,
711
738
  ) -> TaskResponse:
712
739
  """Update an existing task.
713
740
 
@@ -715,11 +742,12 @@ class AsyncFleet:
715
742
  task_key: The key of the task to update
716
743
  prompt: New prompt text for the task (optional)
717
744
  verifier_code: Python code for task verification (optional)
745
+ metadata: Additional metadata for the task (optional)
718
746
 
719
747
  Returns:
720
748
  TaskResponse containing the updated task details
721
749
  """
722
- payload = TaskUpdateRequest(prompt=prompt, verifier_code=verifier_code)
750
+ payload = TaskUpdateRequest(prompt=prompt, verifier_code=verifier_code, metadata=metadata)
723
751
  response = await self.client.request(
724
752
  "PUT", f"/v1/tasks/{task_key}", json=payload.model_dump(exclude_none=True)
725
753
  )
@@ -809,6 +837,11 @@ async def _delete_instance(client: AsyncWrapper, instance_id: str) -> InstanceRe
809
837
  return InstanceResponse(**response.json())
810
838
 
811
839
 
840
+ async def _delete_instances_by_run_id(client: AsyncWrapper, run_id: str) -> List[InstanceResponse]:
841
+ response = await client.request("DELETE", f"/v1/env/instances/run/{run_id}")
842
+ return [InstanceResponse(**instance_data) for instance_data in response.json()]
843
+
844
+
812
845
  async def _check_bundle_exists(
813
846
  client: AsyncWrapper, bundle_hash: str
814
847
  ) -> 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()
@@ -155,12 +155,14 @@ class TaskRequest(BaseModel):
155
155
  verifier_id: Optional[str] = Field(None, title="Verifier Id")
156
156
  version: Optional[str] = Field(None, title="Version")
157
157
  env_variables: Optional[Dict[str, Any]] = Field(None, title="Env Variables")
158
+ metadata: Optional[Dict[str, Any]] = Field(None, title="Metadata")
158
159
  output_json_schema: Optional[Dict[str, Any]] = Field(None, title="Output Json Schema")
159
160
 
160
161
 
161
162
  class TaskUpdateRequest(BaseModel):
162
163
  prompt: Optional[str] = Field(None, title="Prompt")
163
164
  verifier_code: Optional[str] = Field(None, title="Verifier Code")
165
+ metadata: Optional[Dict[str, Any]] = Field(None, title="Metadata")
164
166
 
165
167
 
166
168
  class VerifierData(BaseModel):
@@ -186,6 +188,7 @@ class TaskResponse(BaseModel):
186
188
  data_version: Optional[str] = Field(None, title="Data Version")
187
189
  env_variables: Optional[Dict[str, Any]] = Field(None, title="Env Variables")
188
190
  verifier: Optional[VerifierData] = Field(None, title="Verifier")
191
+ metadata: Optional[Dict[str, Any]] = Field(None, title="Metadata")
189
192
  output_json_schema: Optional[Dict[str, Any]] = Field(None, title="Output Json Schema")
190
193
 
191
194
 
@@ -209,23 +209,27 @@ class Task(BaseModel):
209
209
  )
210
210
  self.verifier = verifier
211
211
 
212
- async def make_env(self, region: Optional[str] = None):
212
+ async def make_env(
213
+ self,
214
+ region: Optional[str] = None,
215
+ image_type: Optional[str] = None,
216
+ ttl_seconds: Optional[int] = None,
217
+ run_id: Optional[str] = None,
218
+ ):
213
219
  """Create an environment instance for this task's environment.
214
220
 
215
- Uses the task's env_id (and version if present) to create the env.
221
+ Alias for make() method. Uses the task's env_id (and version if present) to create the env.
216
222
  """
217
- if not self.env_id:
218
- raise ValueError("Task has no env_id defined")
219
- # Deferred import to avoid circular dependencies
220
- from .client import AsyncFleet
221
-
222
- return await AsyncFleet().make(env_key=self.env_key, region=region)
223
+ return await self.make(
224
+ region=region, image_type=image_type, ttl_seconds=ttl_seconds, run_id=run_id
225
+ )
223
226
 
224
227
  async def make(
225
228
  self,
226
229
  region: Optional[str] = None,
227
230
  image_type: Optional[str] = None,
228
231
  ttl_seconds: Optional[int] = None,
232
+ run_id: Optional[str] = None,
229
233
  ):
230
234
  """Create an environment instance with task's configuration.
231
235
 
@@ -233,11 +237,13 @@ class Task(BaseModel):
233
237
  - env_key (env_id + version)
234
238
  - data_key (data_id + data_version, if present)
235
239
  - env_variables (if present)
240
+ - run_id (if present)
236
241
 
237
242
  Args:
238
243
  region: Optional AWS region for the environment
239
244
  image_type: Optional image type for the environment
240
245
  ttl_seconds: Optional TTL in seconds for the instance
246
+ run_id: Optional run ID to group instances
241
247
 
242
248
  Returns:
243
249
  Environment instance configured for this task
@@ -245,7 +251,7 @@ class Task(BaseModel):
245
251
  Example:
246
252
  task = fleet.Task(key="my-task", prompt="...", env_id="my-env",
247
253
  data_id="my-data", data_version="v1.0")
248
- env = await task.make(region="us-west-2")
254
+ env = await task.make(region="us-west-2", run_id="my-batch-123")
249
255
  """
250
256
  if not self.env_id:
251
257
  raise ValueError("Task has no env_id defined")
@@ -260,6 +266,7 @@ class Task(BaseModel):
260
266
  env_variables=self.env_variables if self.env_variables else None,
261
267
  image_type=image_type,
262
268
  ttl_seconds=ttl_seconds,
269
+ run_id=run_id,
263
270
  )
264
271
 
265
272
 
@@ -279,14 +286,17 @@ def verifier_from_string(
279
286
  """
280
287
  try:
281
288
  import inspect
289
+ import re
282
290
  from .verifiers.verifier import AsyncVerifierFunction
283
291
  from fleet.verifiers.code import TASK_SUCCESSFUL_SCORE, TASK_FAILED_SCORE
284
292
  from fleet.verifiers.db import IgnoreConfig
285
- from fleet.verifiers.parsing import parse_and_validate_verifier
286
293
 
287
- # Validate the code and extract function name
288
- # This ensures no arbitrary code execution during import
289
- func_name = parse_and_validate_verifier(verifier_func)
294
+ # Strip @verifier decorator if present to avoid double-wrapping
295
+ # Remove lines like: @verifier(key="...")
296
+ cleaned_code = re.sub(r"@verifier\([^)]*\)\s*\n", "", verifier_func)
297
+ # Also remove the verifier import if present
298
+ cleaned_code = re.sub(r"from fleet import.*verifier.*\n", "", cleaned_code)
299
+ cleaned_code = re.sub(r"import.*verifier.*\n", "", cleaned_code)
290
300
 
291
301
  # Create a local namespace for executing the code
292
302
  local_namespace = {
@@ -296,18 +306,20 @@ def verifier_from_string(
296
306
  "Environment": object, # Add Environment type if needed
297
307
  }
298
308
 
299
- # Execute the verifier code in the namespace
300
- # This is now safe because we validated it contains only declarative code
301
- exec(verifier_func, globals(), local_namespace)
309
+ # Execute the cleaned verifier code in the namespace
310
+ exec(cleaned_code, globals(), local_namespace)
302
311
 
303
- # Get the function by the parsed function name
304
- func_obj = local_namespace.get(func_name)
312
+ # Find the function that was defined (not imported)
313
+ # Functions defined via exec have co_filename == '<string>'
314
+ # Imported functions have their actual module file path
315
+ func_obj = None
316
+ for name, obj in local_namespace.items():
317
+ if inspect.isfunction(obj) and obj.__code__.co_filename == "<string>":
318
+ func_obj = obj
319
+ break
305
320
 
306
321
  if func_obj is None:
307
- raise ValueError(f"Function '{func_name}' not found in verifier code")
308
-
309
- if not inspect.isfunction(func_obj):
310
- raise ValueError(f"'{func_name}' is not a function")
322
+ raise ValueError("No function found in verifier code")
311
323
 
312
324
  # Create an AsyncVerifierFunction instance with raw code
313
325
  verifier_instance = AsyncVerifierFunction(
@@ -381,7 +393,7 @@ async def load_tasks(
381
393
 
382
394
 
383
395
  async def update_task(
384
- task_key: str, prompt: Optional[str] = None, verifier_code: Optional[str] = None
396
+ task_key: str, prompt: Optional[str] = None, verifier_code: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None
385
397
  ):
386
398
  """Convenience function to update an existing task.
387
399
 
@@ -389,6 +401,7 @@ async def update_task(
389
401
  task_key: The key of the task to update
390
402
  prompt: New prompt text for the task (optional)
391
403
  verifier_code: Python code for task verification (optional)
404
+ metadata: Additional metadata for the task (optional)
392
405
 
393
406
  Returns:
394
407
  TaskResponse containing the updated task details
@@ -396,16 +409,19 @@ async def update_task(
396
409
  Examples:
397
410
  response = await fleet.update_task("my-task", prompt="New prompt text")
398
411
  response = await fleet.update_task("my-task", verifier_code="def verify(env): return True")
412
+ response = await fleet.update_task("my-task", metadata={"seed": 42, "story": "Updated story"})
399
413
  """
400
414
  from .global_client import get_client
401
415
 
402
416
  client = get_client()
403
417
  return await client.update_task(
404
- task_key=task_key, prompt=prompt, verifier_code=verifier_code
418
+ task_key=task_key, prompt=prompt, verifier_code=verifier_code, metadata=metadata
405
419
  )
406
420
 
407
421
 
408
- async def get_task(task_key: str, version_id: Optional[str] = None, team_id: Optional[str] = None):
422
+ async def get_task(
423
+ task_key: str, version_id: Optional[str] = None, team_id: Optional[str] = None
424
+ ):
409
425
  """Convenience function to get a task by key and optional version.
410
426
 
411
427
  Args:
@@ -424,7 +440,9 @@ async def get_task(task_key: str, version_id: Optional[str] = None, team_id: Opt
424
440
  from .global_client import get_client
425
441
 
426
442
  client = get_client()
427
- return await client.get_task(task_key=task_key, version_id=version_id, team_id=team_id)
443
+ return await client.get_task(
444
+ task_key=task_key, version_id=version_id, team_id=team_id
445
+ )
428
446
 
429
447
 
430
448
  async def import_task(task: Task, project_key: Optional[str] = None):
@@ -212,6 +212,7 @@ class Fleet:
212
212
  env_variables: Optional[Dict[str, Any]] = None,
213
213
  image_type: Optional[str] = None,
214
214
  ttl_seconds: Optional[int] = None,
215
+ run_id: Optional[str] = None,
215
216
  ) -> SyncEnv:
216
217
  if ":" in env_key:
217
218
  env_key_part, env_version = env_key.split(":", 1)
@@ -247,6 +248,7 @@ class Fleet:
247
248
  image_type=image_type,
248
249
  created_from="sdk",
249
250
  ttl_seconds=ttl_seconds,
251
+ run_id=run_id,
250
252
  )
251
253
 
252
254
  # Only use region-specific base URL if no custom base URL is set
@@ -269,13 +271,15 @@ class Fleet:
269
271
  return self.make(env_key=f"{task.env_id}:{task.version}")
270
272
 
271
273
  def instances(
272
- self, status: Optional[str] = None, region: Optional[str] = None
274
+ self, status: Optional[str] = None, region: Optional[str] = None, run_id: Optional[str] = None
273
275
  ) -> List[SyncEnv]:
274
276
  params = {}
275
277
  if status:
276
278
  params["status"] = status
277
279
  if region:
278
280
  params["region"] = region
281
+ if run_id:
282
+ params["run_id"] = run_id
279
283
 
280
284
  response = self.client.request("GET", "/v1/env/instances", params=params)
281
285
  return [
@@ -300,6 +304,28 @@ class Fleet:
300
304
  def delete(self, instance_id: str) -> InstanceResponse:
301
305
  return _delete_instance(self.client, instance_id)
302
306
 
307
+ def close(self, instance_id: str) -> InstanceResponse:
308
+ """Close (delete) a specific instance by ID.
309
+
310
+ Args:
311
+ instance_id: The instance ID to close
312
+
313
+ Returns:
314
+ InstanceResponse containing the deleted instance details
315
+ """
316
+ return _delete_instance(self.client, instance_id)
317
+
318
+ def close_all(self, run_id: str) -> List[InstanceResponse]:
319
+ """Close (delete) all instances associated with a run_id.
320
+
321
+ Args:
322
+ run_id: The run ID whose instances should be closed
323
+
324
+ Returns:
325
+ List[InstanceResponse] containing the deleted instances
326
+ """
327
+ return _delete_instances_by_run_id(self.client, run_id)
328
+
303
329
  def load_tasks_from_file(self, filename: str) -> List[Task]:
304
330
  with open(filename, "r", encoding="utf-8") as f:
305
331
  tasks_data = f.read()
@@ -559,7 +585,7 @@ class Fleet:
559
585
  env_variables=task_response.env_variables or {},
560
586
  verifier_func=verifier_func, # Set verifier code
561
587
  verifier=verifier, # Use created verifier or None
562
- metadata={}, # Default empty metadata
588
+ metadata=task_response.metadata or {},
563
589
  output_json_schema=getattr(task_response, "output_json_schema", None), # Get output_json_schema if available
564
590
  )
565
591
  tasks.append(task)
@@ -706,6 +732,7 @@ class Fleet:
706
732
  task_key: str,
707
733
  prompt: Optional[str] = None,
708
734
  verifier_code: Optional[str] = None,
735
+ metadata: Optional[Dict[str, Any]] = None,
709
736
  ) -> TaskResponse:
710
737
  """Update an existing task.
711
738
 
@@ -713,11 +740,12 @@ class Fleet:
713
740
  task_key: The key of the task to update
714
741
  prompt: New prompt text for the task (optional)
715
742
  verifier_code: Python code for task verification (optional)
743
+ metadata: Additional metadata for the task (optional)
716
744
 
717
745
  Returns:
718
746
  TaskResponse containing the updated task details
719
747
  """
720
- payload = TaskUpdateRequest(prompt=prompt, verifier_code=verifier_code)
748
+ payload = TaskUpdateRequest(prompt=prompt, verifier_code=verifier_code, metadata=metadata)
721
749
  response = self.client.request(
722
750
  "PUT", f"/v1/tasks/{task_key}", json=payload.model_dump(exclude_none=True)
723
751
  )
@@ -808,6 +836,11 @@ def _delete_instance(client: SyncWrapper, instance_id: str) -> InstanceResponse:
808
836
  return InstanceResponse(**response.json())
809
837
 
810
838
 
839
+ def _delete_instances_by_run_id(client: SyncWrapper, run_id: str) -> List[InstanceResponse]:
840
+ response = client.request("DELETE", f"/v1/env/instances/run/{run_id}")
841
+ return [InstanceResponse(**instance_data) for instance_data in response.json()]
842
+
843
+
811
844
  def _check_bundle_exists(
812
845
  client: SyncWrapper, bundle_hash: str
813
846
  ) -> VerifiersCheckResponse:
@@ -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
  ]
@@ -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()
@@ -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):
@@ -158,6 +159,7 @@ class TaskRequest(BaseModel):
158
159
  verifier_id: Optional[str] = Field(None, title="Verifier Id")
159
160
  version: Optional[str] = Field(None, title="Version")
160
161
  env_variables: Optional[Dict[str, Any]] = Field(None, title="Env Variables")
162
+ metadata: Optional[Dict[str, Any]] = Field(None, title="Metadata")
161
163
  output_json_schema: Optional[Dict[str, Any]] = Field(
162
164
  None, title="Output Json Schema"
163
165
  )
@@ -166,6 +168,7 @@ class TaskRequest(BaseModel):
166
168
  class TaskUpdateRequest(BaseModel):
167
169
  prompt: Optional[str] = Field(None, title="Prompt")
168
170
  verifier_code: Optional[str] = Field(None, title="Verifier Code")
171
+ metadata: Optional[Dict[str, Any]] = Field(None, title="Metadata")
169
172
 
170
173
 
171
174
  class VerifierData(BaseModel):
@@ -191,6 +194,7 @@ class TaskResponse(BaseModel):
191
194
  data_version: Optional[str] = Field(None, title="Data Version")
192
195
  env_variables: Optional[Dict[str, Any]] = Field(None, title="Env Variables")
193
196
  verifier: Optional[VerifierData] = Field(None, title="Verifier")
197
+ metadata: Optional[Dict[str, Any]] = Field(None, title="Metadata")
194
198
  output_json_schema: Optional[Dict[str, Any]] = Field(
195
199
  None, title="Output Json Schema"
196
200
  )
@@ -360,6 +364,7 @@ class InstanceResponse(BaseModel):
360
364
  data_version: Optional[str] = Field(None, title="Data Version")
361
365
  urls: Optional[InstanceURLs] = Field(None, title="Urls")
362
366
  health: Optional[bool] = Field(None, title="Health")
367
+ run_id: Optional[str] = Field(None, title="Run Id")
363
368
 
364
369
 
365
370
  class AccountResponse(BaseModel):