fleet-python 0.2.29__py3-none-any.whl → 0.2.32__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.

Files changed (61) hide show
  1. examples/diff_example.py +30 -20
  2. examples/dsl_example.py +12 -7
  3. examples/example.py +4 -4
  4. examples/example_account.py +8 -0
  5. examples/example_action_log.py +2 -2
  6. examples/example_client.py +2 -2
  7. examples/example_mcp_anthropic.py +8 -5
  8. examples/example_mcp_openai.py +2 -2
  9. examples/example_sync.py +4 -4
  10. examples/example_task.py +16 -6
  11. examples/example_tasks.py +3 -6
  12. examples/example_verifier.py +16 -3
  13. examples/gemini_example.py +6 -6
  14. examples/json_tasks_example.py +2 -2
  15. examples/nova_act_example.py +2 -2
  16. examples/openai_example.py +3 -3
  17. examples/openai_simple_example.py +3 -3
  18. examples/query_builder_example.py +11 -7
  19. fleet/__init__.py +60 -5
  20. fleet/_async/__init__.py +258 -1
  21. fleet/_async/base.py +2 -1
  22. fleet/_async/client.py +164 -144
  23. fleet/_async/env/client.py +2 -0
  24. fleet/_async/global_client.py +43 -0
  25. fleet/_async/instance/client.py +1 -1
  26. fleet/_async/models.py +172 -171
  27. fleet/_async/resources/base.py +1 -1
  28. fleet/_async/resources/mcp.py +55 -0
  29. fleet/_async/resources/sqlite.py +141 -130
  30. fleet/_async/tasks.py +69 -16
  31. fleet/_async/verifiers/__init__.py +2 -2
  32. fleet/_async/verifiers/bundler.py +18 -14
  33. fleet/_async/verifiers/verifier.py +77 -71
  34. fleet/base.py +2 -1
  35. fleet/client.py +145 -158
  36. fleet/config.py +3 -2
  37. fleet/env/__init__.py +9 -1
  38. fleet/env/client.py +3 -0
  39. fleet/global_client.py +43 -0
  40. fleet/instance/__init__.py +1 -1
  41. fleet/instance/client.py +2 -4
  42. fleet/models.py +172 -171
  43. fleet/resources/base.py +1 -1
  44. fleet/resources/mcp.py +27 -33
  45. fleet/resources/sqlite.py +136 -131
  46. fleet/tasks.py +195 -16
  47. fleet/types.py +1 -1
  48. fleet/verifiers/__init__.py +2 -2
  49. fleet/verifiers/bundler.py +18 -14
  50. fleet/verifiers/code.py +1 -1
  51. fleet/verifiers/decorator.py +25 -34
  52. fleet/verifiers/parse.py +98 -68
  53. fleet/verifiers/verifier.py +77 -78
  54. {fleet_python-0.2.29.dist-info → fleet_python-0.2.32.dist-info}/METADATA +9 -9
  55. fleet_python-0.2.32.dist-info/RECORD +74 -0
  56. scripts/fix_sync_imports.py +87 -59
  57. scripts/unasync.py +10 -9
  58. fleet_python-0.2.29.dist-info/RECORD +0 -70
  59. {fleet_python-0.2.29.dist-info → fleet_python-0.2.32.dist-info}/WHEEL +0 -0
  60. {fleet_python-0.2.29.dist-info → fleet_python-0.2.32.dist-info}/licenses/LICENSE +0 -0
  61. {fleet_python-0.2.29.dist-info → fleet_python-0.2.32.dist-info}/top_level.txt +0 -0
fleet/_async/tasks.py CHANGED
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import re
6
6
  from datetime import datetime
7
- from typing import Any, Dict, Optional
7
+ from typing import Any, Dict, Optional, List
8
8
  from uuid import UUID
9
9
 
10
10
  from pydantic import BaseModel, Field, validator
@@ -15,31 +15,39 @@ from fleet.types import VerifierFunction
15
15
 
16
16
  class Task(BaseModel):
17
17
  """A task model representing a single task in the Fleet system."""
18
-
18
+
19
19
  key: str = Field(..., description="Unique task key identifier")
20
20
  prompt: str = Field(..., description="Task prompt or instruction")
21
21
  env_id: str = Field(..., description="Environment identifier")
22
- env_variables: Optional[Dict[str, Any]] = Field(default_factory=dict, description="Environment variables")
22
+ env_variables: Optional[Dict[str, Any]] = Field(
23
+ default_factory=dict, description="Environment variables"
24
+ )
23
25
  created_at: Optional[datetime] = Field(None, description="Task creation timestamp")
24
26
  version: Optional[str] = Field(None, description="Task version")
25
27
  verifier_func: Optional[str] = Field(None, description="Verifier function code")
26
- verifier: Optional[Any] = Field(None, description="Verifier function with decorator (async or sync)")
28
+ verifier: Optional[Any] = Field(
29
+ None, description="Verifier function with decorator (async or sync)"
30
+ )
27
31
  verifier_id: Optional[str] = Field(None, description="Verifier identifier")
28
32
  verifier_sha: Optional[str] = Field(None, description="Verifier SHA256 hash")
29
- metadata: Optional[Dict[str, Any]] = Field(default_factory=dict, description="Additional task metadata")
33
+ metadata: Optional[Dict[str, Any]] = Field(
34
+ default_factory=dict, description="Additional task metadata"
35
+ )
30
36
 
31
- @validator('key')
37
+ @validator("key")
32
38
  def validate_key_format(cls, v):
33
39
  """Validate key follows kebab-case format."""
34
- if not re.match(r'^[a-z0-9]+(-[a-z0-9]+)*$', v):
35
- raise ValueError(f'Invalid task key format: {v}. Must follow kebab-case format.')
40
+ if not re.match(r"^[a-z0-9]+(-[a-z0-9]+)*$", v):
41
+ raise ValueError(
42
+ f"Invalid task key format: {v}. Must follow kebab-case format."
43
+ )
36
44
  return v
37
45
 
38
- @validator('created_at', pre=True, always=True)
46
+ @validator("created_at", pre=True, always=True)
39
47
  def set_created_at(cls, v):
40
48
  """Set created_at to current time if not provided."""
41
49
  return v or datetime.now()
42
-
50
+
43
51
  @property
44
52
  def env_key(self) -> str:
45
53
  """Get the environment key combining env_id and version."""
@@ -49,24 +57,25 @@ class Task(BaseModel):
49
57
 
50
58
  class Config:
51
59
  """Pydantic model configuration."""
60
+
52
61
  json_encoders = {
53
62
  datetime: lambda v: v.isoformat(),
54
63
  }
55
64
  # Allow arbitrary types for the verifier field
56
- arbitrary_types_allowed = True
65
+ arbitrary_types_allowed = True
57
66
 
58
67
  def verify(self, env, *args, **kwargs) -> float:
59
68
  """Verify the task using the verifier function (sync version).
60
-
69
+
61
70
  For sync environments, calls the sync verifier directly.
62
71
  For async verifiers, automatically runs them with asyncio.run().
63
72
  """
64
73
  if self.verifier:
65
74
  import asyncio
66
75
  import inspect
67
-
76
+
68
77
  result = self.verifier.remote(env, *args, **kwargs)
69
-
78
+
70
79
  # If the result is a coroutine, we need to run it
71
80
  if inspect.iscoroutine(result):
72
81
  # Check if we're already in an event loop
@@ -84,10 +93,10 @@ class Task(BaseModel):
84
93
  return result
85
94
  else:
86
95
  raise ValueError("No verifier function found for this task")
87
-
96
+
88
97
  async def verify_async(self, *args, **kwargs) -> float:
89
98
  """Verify the task using the verifier function (async version).
90
-
99
+
91
100
  For async environments, awaits the async verifier.
92
101
  Works with both sync and async verifiers in async contexts.
93
102
  """
@@ -95,9 +104,53 @@ class Task(BaseModel):
95
104
  result = self.verifier.remote(*args, **kwargs)
96
105
  # If it's a coroutine, await it
97
106
  import inspect
107
+
98
108
  if inspect.iscoroutine(result):
99
109
  return await result
100
110
  else:
101
111
  return result
102
112
  else:
103
113
  raise ValueError("No verifier function found for this task")
114
+
115
+ async def make_env(self, region: Optional[str] = None):
116
+ """Create an environment instance for this task's environment.
117
+
118
+ Uses the task's env_id (and version if present) to create the env.
119
+ """
120
+ if not self.env_id:
121
+ raise ValueError("Task has no env_id defined")
122
+ # Deferred import to avoid circular dependencies
123
+ from .client import AsyncFleet
124
+
125
+ return await AsyncFleet().make(env_key=self.env_key, region=region)
126
+
127
+
128
+ async def load_tasks(
129
+ env_key: Optional[str] = None,
130
+ keys: Optional[List[str]] = None,
131
+ version: Optional[str] = None,
132
+ team_id: Optional[str] = None
133
+ ) -> List[Task]:
134
+ """Convenience function to load tasks with optional filtering.
135
+
136
+ Args:
137
+ env_key: Optional environment key to filter tasks by
138
+ keys: Optional list of task keys to filter by
139
+ version: Optional version to filter tasks by
140
+ team_id: Optional team_id to filter by (admin only)
141
+
142
+ Examples:
143
+ tasks = await fleet.load_tasks(env_key="fira")
144
+ tasks = await fleet.load_tasks(keys=["task1", "task2"])
145
+ tasks = await fleet.load_tasks(env_key="fira", version="v1.0")
146
+ """
147
+ # Use the global client by default so users can pre-configure it once
148
+ from .global_client import get_client
149
+
150
+ client = get_client()
151
+ return await client.load_tasks(
152
+ env_key=env_key,
153
+ keys=keys,
154
+ version=version,
155
+ team_id=team_id
156
+ )
@@ -9,9 +9,9 @@ from .verifier import (
9
9
 
10
10
  __all__ = [
11
11
  "DatabaseSnapshot",
12
- "IgnoreConfig",
12
+ "IgnoreConfig",
13
13
  "SnapshotDiff",
14
14
  "TASK_SUCCESSFUL_SCORE",
15
15
  "verifier",
16
16
  "AsyncVerifierFunction",
17
- ]
17
+ ]
@@ -544,7 +544,9 @@ class FunctionBundler:
544
544
  # Ensure fleet-python is always included
545
545
  if not requirements:
546
546
  requirements = ["fleet-python"]
547
- elif "fleet-python" not in [r.split("==")[0].split(">=")[0] for r in requirements]:
547
+ elif "fleet-python" not in [
548
+ r.split("==")[0].split(">=")[0] for r in requirements
549
+ ]:
548
550
  requirements.append("fleet-python")
549
551
  requirements_file.write_text("\n".join(sorted(set(requirements))))
550
552
 
@@ -663,37 +665,39 @@ class FunctionBundler:
663
665
  logger.warning(f"Failed to extract function {function_name}: {e}")
664
666
 
665
667
  return None
666
-
668
+
667
669
  def _get_function_source_without_decorator(self, func: Callable) -> str:
668
670
  """Get function source code without the @verifier decorator."""
669
671
  source = inspect.getsource(func)
670
- lines = source.split('\n')
671
-
672
+ lines = source.split("\n")
673
+
672
674
  # Find where the function definition starts
673
675
  func_start = -1
674
676
  for i, line in enumerate(lines):
675
- if line.strip().startswith('def '):
677
+ if line.strip().startswith("def "):
676
678
  func_start = i
677
679
  break
678
-
680
+
679
681
  if func_start == -1:
680
682
  # Couldn't find function definition, return original
681
683
  return source
682
-
684
+
683
685
  # Return only from the function definition onward
684
686
  func_lines = lines[func_start:]
685
-
687
+
686
688
  # Remove common indentation
687
689
  if func_lines:
688
690
  # Find minimum indentation (excluding empty lines)
689
- min_indent = float('inf')
691
+ min_indent = float("inf")
690
692
  for line in func_lines:
691
693
  if line.strip():
692
694
  indent = len(line) - len(line.lstrip())
693
695
  min_indent = min(min_indent, indent)
694
-
696
+
695
697
  # Remove the common indentation
696
- if min_indent < float('inf'):
697
- func_lines = [line[min_indent:] if line.strip() else line for line in func_lines]
698
-
699
- return '\n'.join(func_lines)
698
+ if min_indent < float("inf"):
699
+ func_lines = [
700
+ line[min_indent:] if line.strip() else line for line in func_lines
701
+ ]
702
+
703
+ return "\n".join(func_lines)
@@ -19,7 +19,7 @@ from ..client import AsyncEnv
19
19
 
20
20
  logger = logging.getLogger(__name__)
21
21
 
22
- F = TypeVar('F', bound=Callable[..., Any])
22
+ F = TypeVar("F", bound=Callable[..., Any])
23
23
 
24
24
  # Global cache to track which bundle SHAs have been uploaded to S3
25
25
  _uploaded_bundle_shas: Set[str] = set()
@@ -33,7 +33,7 @@ def _get_bundle_sha(bundle_data: bytes) -> str:
33
33
 
34
34
  class AsyncVerifierFunction:
35
35
  """Wrapper for a verified function that supports local execution with env-first pattern."""
36
-
36
+
37
37
  def __init__(
38
38
  self,
39
39
  func: F,
@@ -41,7 +41,7 @@ class AsyncVerifierFunction:
41
41
  extra_requirements: Optional[List[str]] = None,
42
42
  verifier_id: Optional[str] = None,
43
43
  sha256: Optional[str] = None,
44
- raw_code: Optional[str] = None
44
+ raw_code: Optional[str] = None,
45
45
  ):
46
46
  self.func = func
47
47
  self.key = key
@@ -52,10 +52,10 @@ class AsyncVerifierFunction:
52
52
  self._bundle_data: Optional[bytes] = None # Cached bundle data
53
53
  self._raw_code: Optional[str] = raw_code # Store raw code if provided
54
54
  self._is_async = asyncio.iscoroutinefunction(func)
55
-
55
+
56
56
  # Copy function metadata
57
57
  functools.update_wrapper(self, func)
58
-
58
+
59
59
  def _get_or_create_bundle(self) -> tuple[bytes, str]:
60
60
  """Get or create bundle data and return (bundle_data, sha)."""
61
61
  if self._bundle_data is None or self._bundle_sha is None:
@@ -63,68 +63,72 @@ class AsyncVerifierFunction:
63
63
  if self._raw_code:
64
64
  import io
65
65
  import zipfile
66
-
66
+
67
67
  # Create zip bundle directly (matching bundler format)
68
68
  zip_buffer = io.BytesIO()
69
- with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zf:
69
+ with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf:
70
70
  # Add requirements.txt
71
71
  requirements = self.extra_requirements or []
72
72
  if "fleet-python" not in requirements:
73
73
  requirements.append("fleet-python")
74
74
  req_content = "\n".join(requirements)
75
75
  zf.writestr("requirements.txt", req_content)
76
-
76
+
77
77
  # Add verifier.py with the raw code
78
78
  zf.writestr("verifier.py", self._raw_code)
79
-
79
+
80
80
  self._bundle_data = zip_buffer.getvalue()
81
81
  self._bundle_sha = _get_bundle_sha(self._bundle_data)
82
- logger.debug(f"Created bundle from raw code for {self.key} with SHA: {self._bundle_sha}")
82
+ logger.debug(
83
+ f"Created bundle from raw code for {self.key} with SHA: {self._bundle_sha}"
84
+ )
83
85
  else:
84
86
  # Try to create bundle from function source
85
87
  try:
86
88
  self._bundle_data = self._bundler.create_bundle(
87
- self.func,
88
- self.extra_requirements,
89
- self.verifier_id
89
+ self.func, self.extra_requirements, self.verifier_id
90
90
  )
91
91
  self._bundle_sha = _get_bundle_sha(self._bundle_data)
92
- logger.debug(f"Created bundle for {self.key} with SHA: {self._bundle_sha}")
92
+ logger.debug(
93
+ f"Created bundle for {self.key} with SHA: {self._bundle_sha}"
94
+ )
93
95
  except OSError as e:
94
96
  # Can't create bundle - no source and no raw code
95
97
  raise OSError(f"Cannot create bundle for {self.key}: {e}")
96
-
98
+
97
99
  return self._bundle_data, self._bundle_sha
98
-
100
+
99
101
  async def _check_bundle_status(self, env: AsyncEnv) -> tuple[str, bool]:
100
102
  """Check if bundle needs to be uploaded and return (sha, needs_upload)."""
101
103
  bundle_data, bundle_sha = self._get_or_create_bundle()
102
-
104
+
103
105
  # If bundle_data is empty, we're using server-side bundle
104
106
  if not bundle_data:
105
107
  logger.debug(f"Using server-side bundle {bundle_sha[:8]}...")
106
108
  return bundle_sha, False # No upload needed, server has it
107
-
109
+
108
110
  # 1. Check local process cache first
109
111
  if bundle_sha in _uploaded_bundle_shas:
110
112
  logger.debug(f"Bundle {bundle_sha[:8]}... found in local cache")
111
113
  return bundle_sha, False # Already uploaded, no upload needed
112
-
114
+
113
115
  # 2. Check if bundle exists on server (pseudocode)
114
116
  # TODO: Add endpoint to check if bundle SHA exists in S3
115
117
  try:
116
118
  exists = await env.check_bundle_exists(bundle_sha)
117
119
  if exists.success:
118
- logger.info(f"Bundle {bundle_sha[:8]}... found on server, updating cache")
120
+ logger.info(
121
+ f"Bundle {bundle_sha[:8]}... found on server, updating cache"
122
+ )
119
123
  _uploaded_bundle_shas.add(bundle_sha)
120
124
  return bundle_sha, False # Found on server, no upload needed
121
125
  except Exception as e:
122
126
  logger.warning(f"Failed to check bundle existence: {e}")
123
-
127
+
124
128
  # 3. Bundle not found locally or on server - upload needed
125
129
  logger.info(f"Bundle {bundle_sha[:8]}... needs to be uploaded")
126
130
  return bundle_sha, True # Upload needed
127
-
131
+
128
132
  async def __call__(self, env: AsyncEnv, *args, **kwargs) -> float:
129
133
  """Local execution of the verifier function with env as first parameter."""
130
134
  try:
@@ -134,7 +138,7 @@ class AsyncVerifierFunction:
134
138
  else:
135
139
  # For sync functions, call directly
136
140
  result = self.func(env, *args, **kwargs)
137
-
141
+
138
142
  # Handle different return types
139
143
  if isinstance(result, (int, float)):
140
144
  # Direct score return
@@ -144,16 +148,18 @@ class AsyncVerifierFunction:
144
148
  return result
145
149
  else:
146
150
  # Try to extract score from object attributes
147
- if hasattr(result, 'score'):
151
+ if hasattr(result, "score"):
148
152
  return float(result.score)
149
153
  else:
150
- raise ValueError(f"Verifier function must return a score (number). Got {type(result)}")
151
-
154
+ raise ValueError(
155
+ f"Verifier function must return a score (number). Got {type(result)}"
156
+ )
157
+
152
158
  except Exception as e:
153
159
  logger.error(f"Error in verifier {self.key}: {e}")
154
160
  # Return error score 0
155
161
  return 0.0
156
-
162
+
157
163
  async def remote(self, env: AsyncEnv, *args, **kwargs) -> float:
158
164
  """Remote execution of the verifier function with SHA-based bundle caching."""
159
165
  # Async verifiers are now supported by the backend
@@ -163,20 +169,20 @@ class AsyncVerifierFunction:
163
169
  # "The remote execution environment only supports synchronous functions. "
164
170
  # "Please provide a synchronous version of your verifier."
165
171
  # )
166
-
172
+
167
173
  args_array = list(args)
168
174
  args_array.append({"env": env.instance_id})
169
175
  args = tuple(args_array)
170
-
176
+
171
177
  try:
172
178
  # Check if bundle needs to be uploaded
173
179
  bundle_sha, needs_upload = await self._check_bundle_status(env)
174
-
180
+
175
181
  if needs_upload:
176
182
  # Need to upload bundle to S3
177
183
  logger.info(f"Uploading bundle {bundle_sha[:8]}... for {self.key}")
178
184
  bundle_data, _ = self._get_or_create_bundle()
179
-
185
+
180
186
  response = await env.execute_verifier_remote(
181
187
  bundle_data=bundle_data,
182
188
  bundle_sha=bundle_sha,
@@ -185,42 +191,46 @@ class AsyncVerifierFunction:
185
191
  args=args,
186
192
  args_array=args_array,
187
193
  kwargs=kwargs,
188
- needs_upload=True
194
+ needs_upload=True,
189
195
  )
190
-
196
+
191
197
  # Mark as uploaded after successful execution
192
198
  _uploaded_bundle_shas.add(bundle_sha)
193
199
  logger.debug(f"Registered bundle {bundle_sha[:8]}... as uploaded")
194
-
200
+
195
201
  else:
196
202
  # Bundle already available - execute without upload
197
- logger.info(f"Executing cached bundle {bundle_sha[:8]}... for {self.key}")
203
+ logger.info(
204
+ f"Executing cached bundle {bundle_sha[:8]}... for {self.key}"
205
+ )
198
206
  bundle_data, _ = self._get_or_create_bundle()
199
-
207
+
200
208
  response = await env.execute_verifier_remote(
201
- bundle_data=bundle_data or b'', # Empty if using server-side bundle
209
+ bundle_data=bundle_data or b"", # Empty if using server-side bundle
202
210
  bundle_sha=bundle_sha,
203
211
  key=self.key,
204
212
  function_name=self.func.__name__,
205
213
  args=args,
206
214
  args_array=args_array,
207
215
  kwargs=kwargs,
208
- needs_upload=False # Don't upload, just execute
216
+ needs_upload=False, # Don't upload, just execute
209
217
  )
210
-
218
+
211
219
  # Handle response
220
+ if response.stdout:
221
+ print(response.stdout)
212
222
  if response.success:
213
223
  return self._process_result(response.result)
214
224
  else:
215
225
  self._raise_remote_error(response.error)
216
-
226
+
217
227
  except Exception as e:
218
228
  logger.error(f"Remote execution failed for {self.key}: {e}")
219
229
  # If it's an HTTP error, try to get more details
220
- if hasattr(e, 'response') and hasattr(e.response, 'text'):
230
+ if hasattr(e, "response") and hasattr(e.response, "text"):
221
231
  logger.error(f"Server response: {e.response.text}")
222
232
  raise
223
-
233
+
224
234
  def _process_result(self, result: Any) -> float:
225
235
  """Process remote execution result, handling different return types."""
226
236
  # Handle different return types like local execution
@@ -230,7 +240,7 @@ class AsyncVerifierFunction:
230
240
  return float(result["score"])
231
241
  else:
232
242
  # Try to extract score from object attributes
233
- if hasattr(result, 'score'):
243
+ if hasattr(result, "score"):
234
244
  return float(result.score)
235
245
  else:
236
246
  # Best effort conversion
@@ -239,13 +249,13 @@ class AsyncVerifierFunction:
239
249
  except (ValueError, TypeError):
240
250
  logger.warning(f"Could not convert result to float: {result}")
241
251
  return 0.0
242
-
252
+
243
253
  def _raise_remote_error(self, error_info: Dict[str, Any]):
244
254
  """Reconstruct remote error as local exception."""
245
255
  error_type = error_info.get("type", "RuntimeError")
246
256
  message = error_info.get("message", "Remote execution failed")
247
257
  traceback_str = error_info.get("traceback", "")
248
-
258
+
249
259
  # Create a rich error message
250
260
  full_message = f"""
251
261
  Remote verifier execution failed:
@@ -254,32 +264,32 @@ Remote verifier execution failed:
254
264
  Remote traceback:
255
265
  {traceback_str}
256
266
  """.strip()
257
-
267
+
258
268
  # Try to raise the original exception type
259
269
  try:
260
270
  exception_class = getattr(__builtins__, error_type, RuntimeError)
261
271
  raise exception_class(full_message)
262
272
  except:
263
273
  raise RuntimeError(full_message)
264
-
274
+
265
275
  def _get_env_id(self, env: AsyncEnv) -> str:
266
276
  """Generate a unique identifier for the environment."""
267
277
  # Use instance base URL or similar unique identifier
268
- if hasattr(env, 'instance') and hasattr(env.instance, 'base_url'):
278
+ if hasattr(env, "instance") and hasattr(env.instance, "base_url"):
269
279
  return f"{env.instance.base_url}"
270
280
  else:
271
281
  # Fallback to object id (less ideal but works)
272
282
  return str(id(env))
273
-
283
+
274
284
  def _is_bundle_not_found_error(self, error: Exception) -> bool:
275
285
  """Check if the error indicates the bundle was not found on the server."""
276
286
  # Check for common "bundle not found" error patterns
277
287
  error_msg = str(error).lower()
278
288
  return (
279
- "bundle not found" in error_msg or
280
- "verifier not found" in error_msg or
281
- "404" in error_msg or
282
- "not found" in error_msg
289
+ "bundle not found" in error_msg
290
+ or "verifier not found" in error_msg
291
+ or "404" in error_msg
292
+ or "not found" in error_msg
283
293
  )
284
294
 
285
295
 
@@ -287,21 +297,21 @@ def verifier(
287
297
  key: Optional[str] = None,
288
298
  extra_requirements: Optional[List[str]] = None,
289
299
  sha256: Optional[str] = None,
290
- raw_code: Optional[str] = None
300
+ raw_code: Optional[str] = None,
291
301
  ) -> Callable[[F], AsyncVerifierFunction]:
292
302
  """
293
303
  Decorator to create a verifier function with env-first pattern.
294
-
304
+
295
305
  The decorated function must take 'env' as its first parameter, making it explicit
296
306
  that verifiers operate within an environment context. This makes verifiers reusable
297
307
  across different environments.
298
-
308
+
299
309
  Args:
300
310
  key: Optional key for the verifier. Defaults to function name.
301
311
  extra_requirements: Additional PyPI packages needed by the verifier.
302
312
  sha256: Optional SHA256 hash of existing server-side bundle to use.
303
313
  raw_code: Optional raw code to use as bundle (bypasses source extraction).
304
-
314
+
305
315
  Example:
306
316
  # Synchronous verifier (works locally and remotely)
307
317
  @verifier(key="check_user_count")
@@ -310,7 +320,7 @@ def verifier(
310
320
  result = db.query("SELECT COUNT(*) FROM users")
311
321
  actual_count = result.rows[0][0]
312
322
  return 1.0 if actual_count >= expected_count else 0.0
313
-
323
+
314
324
  # Async verifier (only works locally)
315
325
  @verifier(key="check_user_async")
316
326
  async def check_user_async(env, expected_count: int) -> float:
@@ -318,29 +328,25 @@ def verifier(
318
328
  result = await db.query("SELECT COUNT(*) FROM users")
319
329
  actual_count = result.rows[0][0]
320
330
  return 1.0 if actual_count >= expected_count else 0.0
321
-
331
+
322
332
  # Usage
323
- env = await flt.env.make_async("fira")
324
-
333
+ env = await fleet.env.make_async("fira")
334
+
325
335
  # Local execution
326
336
  result = await check_user_count(env, 5) # sync verifier
327
337
  result = await check_user_async(env, 5) # async verifier
328
-
338
+
329
339
  # Remote execution
330
340
  result = await check_user_count.remote(env, 5) # sync verifier works
331
341
  # await check_user_async.remote(env, 5) # raises NotImplementedError
332
342
  """
343
+
333
344
  def decorator(func: F) -> AsyncVerifierFunction:
334
345
  verifier_key = key or func.__name__
335
346
  verifier_uuid = str(uuid.uuid4())
336
-
347
+
337
348
  return AsyncVerifierFunction(
338
- func,
339
- verifier_key,
340
- extra_requirements,
341
- verifier_uuid,
342
- sha256,
343
- raw_code
349
+ func, verifier_key, extra_requirements, verifier_uuid, sha256, raw_code
344
350
  )
345
-
346
- return decorator
351
+
352
+ return decorator
fleet/base.py CHANGED
@@ -46,6 +46,7 @@ class BaseWrapper:
46
46
  headers["Authorization"] = f"Bearer {self.api_key}"
47
47
  # Debug log
48
48
  import logging
49
+
49
50
  logger = logging.getLogger(__name__)
50
51
  logger.debug(f"Headers being sent: {headers}")
51
52
  return headers
@@ -89,7 +90,7 @@ class SyncWrapper(BaseWrapper):
89
90
  def _handle_error_response(self, response: httpx.Response) -> None:
90
91
  """Handle HTTP error responses and convert to appropriate Fleet exceptions."""
91
92
  status_code = response.status_code
92
-
93
+
93
94
  # Debug log 500 errors
94
95
  if status_code == 500:
95
96
  logger.error(f"Got 500 error from {response.url}")