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.
- {fleet_python-0.2.79/fleet_python.egg-info → fleet_python-0.2.80}/PKG-INFO +6 -1
- {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/iterate_verifiers.py +234 -34
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/__init__.py +1 -1
- fleet_python-0.2.80/fleet/cli.py +354 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/client.py +136 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/models.py +135 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80/fleet_python.egg-info}/PKG-INFO +6 -1
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet_python.egg-info/SOURCES.txt +2 -0
- fleet_python-0.2.80/fleet_python.egg-info/entry_points.txt +2 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet_python.egg-info/requires.txt +6 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/pyproject.toml +10 -1
- {fleet_python-0.2.79 → fleet_python-0.2.80}/LICENSE +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/README.md +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/diff_example.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/dsl_example.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/example.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/exampleResume.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/example_account.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/example_action_log.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/example_client.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/example_mcp_anthropic.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/example_mcp_openai.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/example_sync.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/example_task.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/example_tasks.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/example_verifier.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/export_tasks.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/fetch_tasks.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/gemini_example.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/import_tasks.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/json_tasks_example.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/nova_act_example.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/openai_example.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/openai_simple_example.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/query_builder_example.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/quickstart.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/examples/test_cdp_logging.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/_async/__init__.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/_async/base.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/_async/client.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/_async/env/__init__.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/_async/env/client.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/_async/exceptions.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/_async/global_client.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/_async/instance/__init__.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/_async/instance/base.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/_async/instance/client.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/_async/models.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/_async/resources/__init__.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/_async/resources/base.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/_async/resources/browser.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/_async/resources/mcp.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/_async/resources/sqlite.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/_async/tasks.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/_async/verifiers/__init__.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/_async/verifiers/bundler.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/_async/verifiers/verifier.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/base.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/config.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/env/__init__.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/env/client.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/exceptions.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/global_client.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/instance/__init__.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/instance/base.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/instance/client.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/instance/models.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/resources/__init__.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/resources/base.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/resources/browser.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/resources/mcp.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/resources/sqlite.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/tasks.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/types.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/verifiers/__init__.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/verifiers/bundler.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/verifiers/code.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/verifiers/db.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/verifiers/decorator.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/verifiers/parse.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/verifiers/sql_differ.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet/verifiers/verifier.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet_python.egg-info/dependency_links.txt +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/fleet_python.egg-info/top_level.txt +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/scripts/fix_sync_imports.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/scripts/unasync.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/setup.cfg +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/tests/__init__.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/tests/test_app_method.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/tests/test_expect_only.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/tests/test_instance_dispatch.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/tests/test_sqlite_resource_dual_mode.py +0 -0
- {fleet_python-0.2.79 → fleet_python-0.2.80}/tests/test_sqlite_shared_memory_behavior.py +0 -0
- {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.
|
|
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
|
|
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
|
|
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
|
-
|
|
108
|
+
Tuple of (function code, leading_content or None)
|
|
57
109
|
"""
|
|
58
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
291
|
-
|
|
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] =
|
|
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] =
|
|
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
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
#
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
|
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,
|
|
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:
|