fleet-python 0.2.79__tar.gz → 0.2.80__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.
Files changed (94) hide show
  1. {fleet_python-0.2.79/fleet_python.egg-info → fleet_python-0.2.80}/PKG-INFO +6 -1
  2. {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/iterate_verifiers.py +234 -34
  3. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/__init__.py +1 -1
  4. fleet_python-0.2.80/fleet/cli.py +354 -0
  5. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/client.py +136 -0
  6. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/models.py +135 -0
  7. {fleet_python-0.2.79 → fleet_python-0.2.80/fleet_python.egg-info}/PKG-INFO +6 -1
  8. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet_python.egg-info/SOURCES.txt +2 -0
  9. fleet_python-0.2.80/fleet_python.egg-info/entry_points.txt +2 -0
  10. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet_python.egg-info/requires.txt +6 -0
  11. {fleet_python-0.2.79 → fleet_python-0.2.80}/pyproject.toml +10 -1
  12. {fleet_python-0.2.79 → fleet_python-0.2.80}/LICENSE +0 -0
  13. {fleet_python-0.2.79 → fleet_python-0.2.80}/README.md +0 -0
  14. {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/diff_example.py +0 -0
  15. {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/dsl_example.py +0 -0
  16. {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/example.py +0 -0
  17. {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/exampleResume.py +0 -0
  18. {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/example_account.py +0 -0
  19. {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/example_action_log.py +0 -0
  20. {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/example_client.py +0 -0
  21. {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/example_mcp_anthropic.py +0 -0
  22. {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/example_mcp_openai.py +0 -0
  23. {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/example_sync.py +0 -0
  24. {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/example_task.py +0 -0
  25. {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/example_tasks.py +0 -0
  26. {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/example_verifier.py +0 -0
  27. {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/export_tasks.py +0 -0
  28. {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/fetch_tasks.py +0 -0
  29. {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/gemini_example.py +0 -0
  30. {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/import_tasks.py +0 -0
  31. {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/json_tasks_example.py +0 -0
  32. {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/nova_act_example.py +0 -0
  33. {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/openai_example.py +0 -0
  34. {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/openai_simple_example.py +0 -0
  35. {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/query_builder_example.py +0 -0
  36. {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/quickstart.py +0 -0
  37. {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/test_cdp_logging.py +0 -0
  38. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/_async/__init__.py +0 -0
  39. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/_async/base.py +0 -0
  40. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/_async/client.py +0 -0
  41. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/_async/env/__init__.py +0 -0
  42. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/_async/env/client.py +0 -0
  43. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/_async/exceptions.py +0 -0
  44. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/_async/global_client.py +0 -0
  45. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/_async/instance/__init__.py +0 -0
  46. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/_async/instance/base.py +0 -0
  47. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/_async/instance/client.py +0 -0
  48. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/_async/models.py +0 -0
  49. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/_async/resources/__init__.py +0 -0
  50. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/_async/resources/base.py +0 -0
  51. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/_async/resources/browser.py +0 -0
  52. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/_async/resources/mcp.py +0 -0
  53. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/_async/resources/sqlite.py +0 -0
  54. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/_async/tasks.py +0 -0
  55. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/_async/verifiers/__init__.py +0 -0
  56. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/_async/verifiers/bundler.py +0 -0
  57. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/_async/verifiers/verifier.py +0 -0
  58. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/base.py +0 -0
  59. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/config.py +0 -0
  60. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/env/__init__.py +0 -0
  61. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/env/client.py +0 -0
  62. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/exceptions.py +0 -0
  63. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/global_client.py +0 -0
  64. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/instance/__init__.py +0 -0
  65. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/instance/base.py +0 -0
  66. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/instance/client.py +0 -0
  67. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/instance/models.py +0 -0
  68. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/resources/__init__.py +0 -0
  69. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/resources/base.py +0 -0
  70. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/resources/browser.py +0 -0
  71. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/resources/mcp.py +0 -0
  72. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/resources/sqlite.py +0 -0
  73. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/tasks.py +0 -0
  74. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/types.py +0 -0
  75. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/verifiers/__init__.py +0 -0
  76. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/verifiers/bundler.py +0 -0
  77. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/verifiers/code.py +0 -0
  78. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/verifiers/db.py +0 -0
  79. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/verifiers/decorator.py +0 -0
  80. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/verifiers/parse.py +0 -0
  81. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/verifiers/sql_differ.py +0 -0
  82. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/verifiers/verifier.py +0 -0
  83. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet_python.egg-info/dependency_links.txt +0 -0
  84. {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet_python.egg-info/top_level.txt +0 -0
  85. {fleet_python-0.2.79 → fleet_python-0.2.80}/scripts/fix_sync_imports.py +0 -0
  86. {fleet_python-0.2.79 → fleet_python-0.2.80}/scripts/unasync.py +0 -0
  87. {fleet_python-0.2.79 → fleet_python-0.2.80}/setup.cfg +0 -0
  88. {fleet_python-0.2.79 → fleet_python-0.2.80}/tests/__init__.py +0 -0
  89. {fleet_python-0.2.79 → fleet_python-0.2.80}/tests/test_app_method.py +0 -0
  90. {fleet_python-0.2.79 → fleet_python-0.2.80}/tests/test_expect_only.py +0 -0
  91. {fleet_python-0.2.79 → fleet_python-0.2.80}/tests/test_instance_dispatch.py +0 -0
  92. {fleet_python-0.2.79 → fleet_python-0.2.80}/tests/test_sqlite_resource_dual_mode.py +0 -0
  93. {fleet_python-0.2.79 → fleet_python-0.2.80}/tests/test_sqlite_shared_memory_behavior.py +0 -0
  94. {fleet_python-0.2.79 → fleet_python-0.2.80}/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.79
3
+ Version: 0.2.80
4
4
  Summary: Python SDK for Fleet environments
5
5
  Author-email: Fleet AI <nic@fleet.so>
6
6
  License: Apache-2.0
@@ -26,6 +26,9 @@ Requires-Dist: httpx-retries>=0.4.0
26
26
  Requires-Dist: typing-extensions>=4.0.0
27
27
  Requires-Dist: modulegraph2>=0.2.0
28
28
  Requires-Dist: cloudpickle==3.1.1
29
+ Provides-Extra: cli
30
+ Requires-Dist: typer>=0.9.0; extra == "cli"
31
+ Requires-Dist: rich>=10.0.0; extra == "cli"
29
32
  Provides-Extra: dev
30
33
  Requires-Dist: pytest>=7.0.0; extra == "dev"
31
34
  Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
@@ -35,6 +38,8 @@ Requires-Dist: mypy>=1.0.0; extra == "dev"
35
38
  Requires-Dist: ruff>=0.1.0; extra == "dev"
36
39
  Requires-Dist: unasync>=0.6.0; extra == "dev"
37
40
  Requires-Dist: python-dotenv>=1.1.1; extra == "dev"
41
+ Requires-Dist: typer>=0.9.0; extra == "dev"
42
+ Requires-Dist: rich>=10.0.0; extra == "dev"
38
43
  Provides-Extra: playwright
39
44
  Requires-Dist: playwright>=1.40.0; extra == "playwright"
40
45
  Dynamic: license-file
@@ -5,6 +5,14 @@ import sys
5
5
  from typing import Dict, Tuple, Optional
6
6
 
7
7
 
8
+ # Marker for storing leading content (docstrings, imports) in the Python file
9
+ LEADING_CONTENT_START = "# @LEADING_CONTENT_START"
10
+ LEADING_CONTENT_END = "# @LEADING_CONTENT_END"
11
+ # Legacy markers for backwards compatibility
12
+ LEADING_DOCSTRING_START = "# @LEADING_DOCSTRING_START"
13
+ LEADING_DOCSTRING_END = "# @LEADING_DOCSTRING_END"
14
+
15
+
8
16
  def extract_function_info(function_code: str) -> Optional[Tuple[str, bool]]:
9
17
  """
10
18
  Extract function name and async status from Python function code.
@@ -45,18 +53,61 @@ def extract_function_info(function_code: str) -> Optional[Tuple[str, bool]]:
45
53
  return None
46
54
 
47
55
 
48
- def clean_verifier_code(code: str) -> str:
56
+ def extract_leading_content(code: str) -> Tuple[Optional[str], str]:
57
+ """
58
+ Extract leading content (docstrings, imports, etc.) before the main function definition.
59
+
60
+ Args:
61
+ code: The verifier code
62
+
63
+ Returns:
64
+ Tuple of (leading_content or None, function_code)
65
+ """
66
+ code = code.strip()
67
+
68
+ # Find the first top-level function definition (def or async def at column 0)
69
+ # We need to find "def " or "async def " that's not indented
70
+ lines = code.split("\n")
71
+ func_start_idx = None
72
+
73
+ for i, line in enumerate(lines):
74
+ # Check for unindented def or async def
75
+ if line.startswith("def ") or line.startswith("async def "):
76
+ func_start_idx = i
77
+ break
78
+
79
+ if func_start_idx is None or func_start_idx == 0:
80
+ # No leading content or function not found
81
+ return (None, code)
82
+
83
+ # Everything before the function is leading content
84
+ leading_lines = lines[:func_start_idx]
85
+ func_lines = lines[func_start_idx:]
86
+
87
+ # Clean up leading content - remove empty lines at the end
88
+ while leading_lines and not leading_lines[-1].strip():
89
+ leading_lines.pop()
90
+
91
+ if not leading_lines:
92
+ return (None, code)
93
+
94
+ leading_content = "\n".join(leading_lines)
95
+ function_code = "\n".join(func_lines)
96
+
97
+ return (leading_content, function_code)
98
+
99
+
100
+ def clean_verifier_code(code: str) -> Tuple[str, Optional[str]]:
49
101
  """
50
- Clean verifier code by removing markdown code fences and normalizing whitespace.
102
+ Clean verifier code by removing markdown code fences and extracting leading content.
51
103
 
52
104
  Args:
53
105
  code: Raw verifier code string
54
106
 
55
107
  Returns:
56
- Cleaned code string
108
+ Tuple of (function code, leading_content or None)
57
109
  """
58
- # Normalize escaped newlines
59
- code = code.replace("\\n", "\n").strip()
110
+ code = code.strip()
60
111
 
61
112
  # Remove markdown code fences if present
62
113
  if "```" in code:
@@ -64,7 +115,75 @@ def clean_verifier_code(code: str) -> str:
64
115
  if fence_blocks:
65
116
  code = fence_blocks[0].strip()
66
117
 
67
- return code
118
+ # Extract leading content (docstrings, imports, etc.) if present
119
+ leading_content, code = extract_leading_content(code)
120
+
121
+ return (code, leading_content)
122
+
123
+
124
+ def format_leading_content_as_comment(content: str) -> str:
125
+ """
126
+ Format leading content (docstrings, imports, etc.) as a comment block with markers.
127
+
128
+ Args:
129
+ content: The leading content (docstrings, imports, etc.)
130
+
131
+ Returns:
132
+ Formatted comment block
133
+ """
134
+ lines = [LEADING_CONTENT_START]
135
+
136
+ for line in content.split("\n"):
137
+ # Prefix each line with "# |" to preserve exact content including empty lines
138
+ lines.append(f"# |{line}")
139
+
140
+ lines.append(LEADING_CONTENT_END)
141
+ return "\n".join(lines)
142
+
143
+
144
+ def parse_leading_content_from_comments(comment_block: str) -> str:
145
+ """
146
+ Parse leading content from a comment block with markers.
147
+
148
+ Args:
149
+ comment_block: The comment block between markers
150
+
151
+ Returns:
152
+ Reconstructed leading content
153
+ """
154
+ lines = []
155
+ for line in comment_block.split("\n"):
156
+ # Remove "# |" prefix (new format)
157
+ if line.startswith("# |"):
158
+ lines.append(line[3:])
159
+ # Legacy format: "# " prefix
160
+ elif line.startswith("# "):
161
+ lines.append(line[2:])
162
+ elif line == "#":
163
+ lines.append("")
164
+
165
+ return "\n".join(lines)
166
+
167
+
168
+ def parse_legacy_docstring_from_comments(comment_block: str) -> str:
169
+ """
170
+ Parse a docstring from legacy comment block with markers.
171
+
172
+ Args:
173
+ comment_block: The comment block between markers
174
+
175
+ Returns:
176
+ Reconstructed docstring with triple quotes
177
+ """
178
+ lines = []
179
+ for line in comment_block.split("\n"):
180
+ # Remove "# " prefix
181
+ if line.startswith("# "):
182
+ lines.append(line[2:])
183
+ elif line == "#":
184
+ lines.append("")
185
+
186
+ return '"""' + "\n".join(lines) + '"""'
68
187
 
69
188
 
70
189
  def extract_verifiers_to_file(json_path: str, py_path: str) -> None:
@@ -123,8 +242,8 @@ def extract_verifiers_to_file(json_path: str, py_path: str) -> None:
123
242
  missing_verifier.append(task_key)
124
243
  continue
125
244
 
126
- # Clean the code
127
- cleaned_code = clean_verifier_code(verifier_code)
245
+ # Clean the code and extract leading content (docstrings, imports, etc.)
246
+ cleaned_code, leading_content = clean_verifier_code(verifier_code)
128
247
 
129
248
  # Extract function info
130
249
  func_info = extract_function_info(cleaned_code)
@@ -142,6 +261,7 @@ def extract_verifiers_to_file(json_path: str, py_path: str) -> None:
142
261
  "function_name": function_name,
143
262
  "is_async": is_async,
144
263
  "code": cleaned_code,
264
+ "leading_content": leading_content,
145
265
  }
146
266
  )
147
267
 
@@ -195,7 +315,7 @@ def extract_verifiers_to_file(json_path: str, py_path: str) -> None:
195
315
  f.write("import json\n")
196
316
  f.write("import re\n")
197
317
  f.write("import string\n")
198
- f.write("from typing import Any\n")
318
+ f.write("from typing import Any, Dict, List\n")
199
319
  f.write("\n")
200
320
  f.write("# Helper functions available in verifier namespace\n")
201
321
  f.write(
@@ -248,6 +368,11 @@ def extract_verifiers_to_file(json_path: str, py_path: str) -> None:
248
368
  f"# Function: {ver['function_name']} ({'async' if ver['is_async'] else 'sync'})\n"
249
369
  )
250
370
 
371
+ # Write leading content (docstrings, imports) as comments if present
372
+ if ver["leading_content"]:
373
+ f.write(format_leading_content_as_comment(ver["leading_content"]))
374
+ f.write("\n")
375
+
251
376
  # Write decorator - use verifier for async, verifier_sync for sync
252
377
  decorator_name = "verifier" if ver["is_async"] else "verifier_sync"
253
378
  f.write(f'@{decorator_name}(key="{ver["task_key"]}")\n')
@@ -262,7 +387,7 @@ def extract_verifiers_to_file(json_path: str, py_path: str) -> None:
262
387
  print(f" 2. Run: python {sys.argv[0]} apply {json_path} {py_path}")
263
388
 
264
389
 
265
- def parse_verifiers_from_file(python_path: str) -> Dict[str, str]:
390
+ def parse_verifiers_from_file(python_path: str) -> Dict[str, dict]:
266
391
  """
267
392
  Parse verifiers from a Python file and extract them by task key.
268
393
 
@@ -270,7 +395,7 @@ def parse_verifiers_from_file(python_path: str) -> Dict[str, str]:
270
395
  python_path: Path to Python file containing verifiers
271
396
 
272
397
  Returns:
273
- Dictionary mapping task_key to verifier code
398
+ Dictionary mapping task_key to dict with 'code' and 'leading_content'
274
399
  """
275
400
  print(f"Reading verifiers from: {python_path}")
276
401
 
@@ -281,14 +406,15 @@ def parse_verifiers_from_file(python_path: str) -> Dict[str, str]:
281
406
  print(f"✗ Error: File '{python_path}' not found")
282
407
  sys.exit(1)
283
408
 
284
- # Split content by the separator comments to get individual verifier sections
285
- # The separator is "# ------------------------------------------------------------------------------"
286
- # Each section starts with "# Task: <key>"
287
-
288
409
  verifiers = {}
289
410
 
290
- # Split by "# Task: " markers to find each verifier block
291
- task_blocks = re.split(r"\n# Task: ", content)
411
+ # Split by "# Task: " markers followed by a task key pattern (uuid or specific format)
412
+ # This avoids splitting on "# Task: " that appears inside docstring comments
413
+ # Task keys look like: task_uuid, task_xxx_timestamp_xxx, or send_xxx_xxx
414
+ task_key_pattern = (
415
+ r"(?:task_[a-f0-9-]+|task_[a-z0-9]+_\d+_[a-z0-9]+|[a-z_]+_[a-z0-9]+)"
416
+ )
417
+ task_blocks = re.split(rf"\n# Task: (?={task_key_pattern})", content)
292
418
 
293
419
  for block in task_blocks[1:]: # Skip the first block (header)
294
420
  # Extract task key from the first line
@@ -299,6 +425,10 @@ def parse_verifiers_from_file(python_path: str) -> Dict[str, str]:
299
425
  # First line should be the task key
300
426
  task_key = lines[0].strip()
301
427
 
428
+ # Skip if this doesn't look like a task key (sanity check)
429
+ if not re.match(task_key_pattern, task_key):
430
+ continue
431
+
302
432
  # Find the @verifier or @verifier_sync decorator to extract the key parameter
303
433
  verifier_match = re.search(
304
434
  r'@verifier(?:_sync)?\(key=["\']([^"\']+)["\']\s*(?:,\s*[^)]+)?\)', block
@@ -306,6 +436,26 @@ def parse_verifiers_from_file(python_path: str) -> Dict[str, str]:
306
436
  if verifier_match:
307
437
  task_key = verifier_match.group(1)
308
438
 
439
+ # Check for leading content markers (new format)
440
+ leading_content = None
441
+ if LEADING_CONTENT_START in block:
442
+ start_idx = block.find(LEADING_CONTENT_START)
443
+ end_idx = block.find(LEADING_CONTENT_END)
444
+ if start_idx != -1 and end_idx != -1:
445
+ comment_block = block[
446
+ start_idx + len(LEADING_CONTENT_START) : end_idx
447
+ ].strip()
448
+ leading_content = parse_leading_content_from_comments(comment_block)
449
+ # Fallback: check for legacy docstring markers
450
+ elif LEADING_DOCSTRING_START in block:
451
+ start_idx = block.find(LEADING_DOCSTRING_START)
452
+ end_idx = block.find(LEADING_DOCSTRING_END)
453
+ if start_idx != -1 and end_idx != -1:
454
+ comment_block = block[
455
+ start_idx + len(LEADING_DOCSTRING_START) : end_idx
456
+ ].strip()
457
+ leading_content = parse_legacy_docstring_from_comments(comment_block)
458
+
309
459
  # Find the function definition (async def or def)
310
460
  # Extract from the function start until we hit the separator or end
311
461
  func_pattern = r"((async\s+)?def\s+\w+.*?)(?=\n# -+\n|\n# Task:|\Z)"
@@ -313,26 +463,31 @@ def parse_verifiers_from_file(python_path: str) -> Dict[str, str]:
313
463
 
314
464
  if func_match:
315
465
  function_code = func_match.group(1).strip()
316
- verifiers[task_key] = function_code
466
+ verifiers[task_key] = {
467
+ "code": function_code,
468
+ "leading_content": leading_content,
469
+ }
317
470
 
318
471
  # If the above approach didn't work, try a direct pattern match
319
472
  if not verifiers:
320
473
  # Pattern to match @verifier or @verifier_sync decorator with key and the following function
321
- # Look for the decorator, then capture everything until we hit a dedented line or separator
322
474
  pattern = r'@verifier(?:_sync)?\(key=["\']([^"\']+)["\']\s*(?:,\s*[^)]+)?\)\s*\n((?:async\s+)?def\s+[^\n]+:(?:\n(?: |\t).*)*(?:\n(?: |\t).*)*)'
323
475
 
324
476
  matches = re.findall(pattern, content, re.MULTILINE)
325
477
 
326
478
  for task_key, function_code in matches:
327
- verifiers[task_key] = function_code.strip()
479
+ verifiers[task_key] = {
480
+ "code": function_code.strip(),
481
+ "leading_content": None,
482
+ }
328
483
 
329
484
  print(f"✓ Found {len(verifiers)} verifier(s)")
330
485
 
331
486
  # Analyze async vs sync
332
487
  async_count = 0
333
488
  sync_count = 0
334
- for code in verifiers.values():
335
- func_info = extract_function_info(code)
489
+ for data in verifiers.values():
490
+ func_info = extract_function_info(data["code"])
336
491
  if func_info:
337
492
  _, is_async = func_info
338
493
  if is_async:
@@ -346,6 +501,22 @@ def parse_verifiers_from_file(python_path: str) -> Dict[str, str]:
346
501
  return verifiers
347
502
 
348
503
 
504
+ def normalize_code_for_comparison(code: str) -> str:
505
+ """
506
+ Normalize code for comparison to avoid false positives.
507
+ Removes leading/trailing whitespace and normalizes line endings.
508
+ """
509
+ # Strip and normalize line endings
510
+ code = code.strip().replace("\r\n", "\n")
511
+ # Normalize trailing whitespace on each line
512
+ lines = code.split("\n")
513
+ lines = [line.rstrip() for line in lines]
514
+ code = "\n".join(lines)
515
+ # Normalize multiple blank lines to single (2+ newlines → 1)
516
+ code = re.sub(r"\n\n+", "\n", code)
517
+ return code
518
+
519
+
349
520
  def apply_verifiers_to_json(json_path: str, python_path: str) -> None:
350
521
  """
351
522
  Apply verifiers from Python file back into JSON task file (updates in-place).
@@ -386,17 +557,43 @@ def apply_verifiers_to_json(json_path: str, python_path: str) -> None:
386
557
  continue
387
558
 
388
559
  if task_key in verifiers:
389
- new_code = verifiers[task_key]
390
-
391
- # Escape newlines in debug print patterns (>>> and <<<)
392
- # These should be \n escape sequences, not actual newlines
393
- new_code = new_code.replace(">>>\n", ">>>\\n")
394
- new_code = new_code.replace("\n<<<", "\\n<<<")
395
-
396
- old_code = task.get("verifier_func", "").strip()
560
+ ver_data = verifiers[task_key]
561
+
562
+ # Reconstruct the full verifier code with leading content if present
563
+ if ver_data["leading_content"]:
564
+ new_code = ver_data["leading_content"] + "\n" + ver_data["code"]
565
+ else:
566
+ new_code = ver_data["code"]
567
+
568
+ old_code = task.get("verifier_func", "")
569
+
570
+ # Normalize both for comparison
571
+ old_normalized = normalize_code_for_comparison(old_code)
572
+ new_normalized = normalize_code_for_comparison(new_code)
573
+
574
+ # Debug: show comparison info
575
+ old_len = len(old_normalized)
576
+ new_len = len(new_normalized)
577
+
578
+ if old_normalized == new_normalized:
579
+ if old_code != new_code:
580
+ print(
581
+ f" [DEBUG] {task_key}: Codes differ in whitespace only (normalized match)"
582
+ )
583
+ else:
584
+ # Find first difference position for debugging
585
+ min_len = min(old_len, new_len)
586
+ diff_pos = min_len
587
+ for i in range(min_len):
588
+ if old_normalized[i] != new_normalized[i]:
589
+ diff_pos = i
590
+ break
591
+ print(
592
+ f" [DEBUG] {task_key}: Code changed (old={old_len}, new={new_len}, first_diff@{diff_pos})"
593
+ )
397
594
 
398
595
  # Only update if the code actually changed
399
- if old_code != new_code.strip():
596
+ if old_normalized != new_normalized:
400
597
  # Update verifier_func with new code
401
598
  task["verifier_func"] = new_code
402
599
 
@@ -451,14 +648,17 @@ def validate_verifiers_file(python_path: str) -> None:
451
648
  print("\nValidating verifiers...")
452
649
  errors = []
453
650
 
454
- for task_key, code in verifiers.items():
455
- func_info = extract_function_info(code)
651
+ for task_key, ver_data in verifiers.items():
652
+ func_info = extract_function_info(ver_data["code"])
456
653
  if not func_info:
457
654
  errors.append(f" - {task_key}: Could not extract function info")
458
655
  else:
459
656
  function_name, is_async = func_info
657
+ has_leading = (
658
+ " (has leading content)" if ver_data["leading_content"] else ""
659
+ )
460
660
  print(
461
- f" ✓ {task_key}: {function_name} ({'async' if is_async else 'sync'})"
661
+ f" ✓ {task_key}: {function_name} ({'async' if is_async else 'sync'}){has_leading}"
462
662
  )
463
663
 
464
664
  if errors:
@@ -73,7 +73,7 @@ from . import env
73
73
  from . import global_client as _global_client
74
74
  from ._async import global_client as _async_global_client
75
75
 
76
- __version__ = "0.2.79"
76
+ __version__ = "0.2.80"
77
77
 
78
78
  __all__ = [
79
79
  # Core classes