clerk-sdk 0.5.1__tar.gz → 0.5.3__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 (85) hide show
  1. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/PKG-INFO +3 -2
  2. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/README.md +1 -1
  3. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/__init__.py +1 -1
  4. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/development/cli.py +57 -4
  5. clerk_sdk-0.5.3/clerk/development/code_runner.py +384 -0
  6. clerk_sdk-0.5.3/clerk/development/gui/graph_checker.py +216 -0
  7. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/development/gui/test_session.py +3 -3
  8. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/development/init_project.py +22 -0
  9. clerk_sdk-0.5.3/clerk/development/templates/launch.json.template +20 -0
  10. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/development/templates/main_gui.py.template +3 -3
  11. clerk_sdk-0.5.3/clerk/development/templates/tasks.json.template +25 -0
  12. clerk_sdk-0.5.3/clerk/development/templates/test_payload.py.template +32 -0
  13. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/models/document.py +1 -2
  14. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/pyproject.toml +2 -1
  15. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/requirements.txt +1 -0
  16. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/setup.py +1 -1
  17. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/tests/test_client.py +7 -3
  18. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/tests/test_document_models.py +11 -5
  19. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/uv.lock +27 -0
  20. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/.github/workflows/ci.yaml +0 -0
  21. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/.github/workflows/pypi_publish.yml +0 -0
  22. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/.gitignore +0 -0
  23. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/LICENSE +0 -0
  24. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/MANIFEST.in +0 -0
  25. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/base.py +0 -0
  26. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/client.py +0 -0
  27. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/decorator/__init__.py +0 -0
  28. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/decorator/models.py +0 -0
  29. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/decorator/task_decorator.py +0 -0
  30. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/development/__init__.py +0 -0
  31. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/development/schema/fetch_schema.py +0 -0
  32. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/development/templates/exceptions.py.template +0 -0
  33. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/development/templates/main_basic.py.template +0 -0
  34. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/development/templates/rollbacks.py.template +0 -0
  35. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/development/templates/states.py.template +0 -0
  36. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/development/templates/transitions.py.template +0 -0
  37. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/exceptions/__init__.py +0 -0
  38. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/exceptions/exceptions.py +0 -0
  39. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/exceptions/remote_device.py +0 -0
  40. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/gui_automation/__init__.py +0 -0
  41. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/gui_automation/action_model/__init__.py +0 -0
  42. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/gui_automation/action_model/model.py +0 -0
  43. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/gui_automation/action_model/utils.py +0 -0
  44. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/gui_automation/client.py +0 -0
  45. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/gui_automation/client_actor/__init__.py +0 -0
  46. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/gui_automation/client_actor/client_actor.py +0 -0
  47. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/gui_automation/client_actor/exception.py +0 -0
  48. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/gui_automation/client_actor/model.py +0 -0
  49. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/gui_automation/decorators/__init__.py +0 -0
  50. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/gui_automation/decorators/gui_automation.py +0 -0
  51. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/gui_automation/exceptions/__init__.py +0 -0
  52. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/gui_automation/exceptions/agent_manager.py +0 -0
  53. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/gui_automation/exceptions/modality/__init__.py +0 -0
  54. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/gui_automation/exceptions/modality/exc.py +0 -0
  55. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/gui_automation/exceptions/websocket.py +0 -0
  56. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/gui_automation/requirements.txt +0 -0
  57. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/gui_automation/ui_actions/__init__.py +0 -0
  58. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/gui_automation/ui_actions/actions.py +0 -0
  59. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/gui_automation/ui_actions/base.py +0 -0
  60. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/gui_automation/ui_actions/support.py +0 -0
  61. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/gui_automation/ui_state_inspector/__init__.py +0 -0
  62. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/gui_automation/ui_state_inspector/gui_vision.py +0 -0
  63. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/gui_automation/ui_state_inspector/models.py +0 -0
  64. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/gui_automation/ui_state_machine/Readme.md +0 -0
  65. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/gui_automation/ui_state_machine/__init__.py +0 -0
  66. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/gui_automation/ui_state_machine/ai_recovery.py +0 -0
  67. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/gui_automation/ui_state_machine/decorators.py +0 -0
  68. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/gui_automation/ui_state_machine/exceptions.py +0 -0
  69. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/gui_automation/ui_state_machine/state_machine.py +0 -0
  70. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/models/__init__.py +0 -0
  71. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/models/document_statuses.py +0 -0
  72. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/models/file.py +0 -0
  73. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/models/remote_device.py +0 -0
  74. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/models/response_model.py +0 -0
  75. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/models/ui_operator.py +0 -0
  76. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/utils/__init__.py +0 -0
  77. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/utils/logger.py +0 -0
  78. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/clerk/utils/save_artifact.py +0 -0
  79. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/tests/conftest.py +0 -0
  80. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/tests/test_base.py +0 -0
  81. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/tests/test_exceptions.py +0 -0
  82. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/tests/test_file_models.py +0 -0
  83. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/tests/test_gui_automation.py +0 -0
  84. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/tests/test_task_decorator.py +0 -0
  85. {clerk_sdk-0.5.1 → clerk_sdk-0.5.3}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clerk-sdk
3
- Version: 0.5.1
3
+ Version: 0.5.3
4
4
  Summary: Library for interacting with Clerk
5
5
  Project-URL: Homepage, https://github.com/F-ONE-Group/clerk_pypi
6
6
  Author-email: F-One <contact@f-one.group>
@@ -10,6 +10,7 @@ Classifier: Operating System :: OS Independent
10
10
  Classifier: Programming Language :: Python :: 3
11
11
  Requires-Python: >=3.11
12
12
  Requires-Dist: backoff<3.0.0,>=2.0.0
13
+ Requires-Dist: debugpy<2.0.0,>=1.8.0
13
14
  Requires-Dist: pydantic<3.0.0,>=2.0.0
14
15
  Requires-Dist: python-dotenv>=1.0.0
15
16
  Requires-Dist: requests<3.0.0,>=2.32.3
@@ -128,7 +129,7 @@ Use `UploadDocumentRequest` to send metadata and file attachments. Files can be
128
129
  from clerk.models.document import UploadDocumentRequest
129
130
 
130
131
  upload_request = UploadDocumentRequest(
131
- project_id="proj_456",
132
+ workflow_id="proj_456",
132
133
  message_subject="Invoice 2024-01",
133
134
  files=["/path/to/invoice.pdf"],
134
135
  input_structured_data={"customer_id": "cust_789"},
@@ -104,7 +104,7 @@ Use `UploadDocumentRequest` to send metadata and file attachments. Files can be
104
104
  from clerk.models.document import UploadDocumentRequest
105
105
 
106
106
  upload_request = UploadDocumentRequest(
107
- project_id="proj_456",
107
+ workflow_id="proj_456",
108
108
  message_subject="Invoice 2024-01",
109
109
  files=["/path/to/invoice.pdf"],
110
110
  input_structured_data={"customer_id": "cust_789"},
@@ -1,4 +1,4 @@
1
1
  from .client import Clerk
2
2
 
3
3
 
4
- __version__ = "0.5.1"
4
+ __version__ = "0.5.3"
@@ -53,26 +53,58 @@ def main():
53
53
  help="GUI automation commands"
54
54
  )
55
55
  gui_subparsers = gui_parser.add_subparsers(dest="gui_command", help="GUI subcommands")
56
-
56
+
57
57
  # GUI connect subcommand
58
58
  gui_connect_parser = gui_subparsers.add_parser(
59
59
  "connect",
60
60
  help="Start interactive GUI automation test session"
61
61
  )
62
62
 
63
+ # GUI graph check subcommand
64
+ gui_graph_parser = gui_subparsers.add_parser(
65
+ "graph", help="Graph analysis commands"
66
+ )
67
+ gui_graph_subparsers = gui_graph_parser.add_subparsers(
68
+ dest="graph_command", help="Graph subcommands"
69
+ )
70
+
71
+ gui_graph_check_parser = gui_graph_subparsers.add_parser(
72
+ "check", help="Check and visualize state machine graph structure"
73
+ )
74
+ gui_graph_check_parser.add_argument(
75
+ "--module-path",
76
+ type=str,
77
+ required=False,
78
+ default=None,
79
+ help="Path to the Python file containing the state machine (defaults to src/main.py)",
80
+ )
81
+
63
82
  # Schema command group
64
83
  schema_parser = subparsers.add_parser(
65
84
  "schema",
66
85
  help="Schema management commands"
67
86
  )
68
87
  schema_subparsers = schema_parser.add_subparsers(dest="schema_command", help="Schema subcommands")
69
-
88
+
70
89
  # Schema fetch subcommand
71
90
  schema_fetch_parser = schema_subparsers.add_parser(
72
91
  "fetch",
73
92
  help="Fetch and generate Pydantic models from project schema"
74
93
  )
75
94
 
95
+ # Code command group
96
+ code_parser = subparsers.add_parser(
97
+ "code", help="Custom code development and testing commands"
98
+ )
99
+ code_subparsers = code_parser.add_subparsers(
100
+ dest="code_command", help="Code subcommands"
101
+ )
102
+
103
+ # Code run subcommand
104
+ code_run_parser = code_subparsers.add_parser(
105
+ "run", help="Run custom code with test payloads"
106
+ )
107
+
76
108
  args = parser.parse_args()
77
109
 
78
110
  # Show help if no command specified
@@ -90,16 +122,27 @@ def main():
90
122
  if not hasattr(args, 'gui_command') or not args.gui_command:
91
123
  gui_parser.print_help()
92
124
  sys.exit(1)
93
-
125
+
94
126
  if args.gui_command == "connect":
95
127
  from clerk.development.gui.test_session import main as gui_main
96
128
  gui_main()
97
129
 
130
+ elif args.gui_command == "graph":
131
+ if not hasattr(args, "graph_command") or not args.graph_command:
132
+ print("Error: graph command requires a subcommand")
133
+ print("Available subcommands: check")
134
+ sys.exit(1)
135
+
136
+ if args.graph_command == "check":
137
+ from clerk.development.gui.graph_checker import check_graph
138
+
139
+ check_graph(args.module_path)
140
+
98
141
  elif args.command == "schema":
99
142
  if not hasattr(args, 'schema_command') or not args.schema_command:
100
143
  schema_parser.print_help()
101
144
  sys.exit(1)
102
-
145
+
103
146
  if args.schema_command == "fetch":
104
147
  from clerk.development.schema.fetch_schema import main_with_args
105
148
  project_id = os.getenv("PROJECT_ID")
@@ -108,6 +151,16 @@ def main():
108
151
  sys.exit(1)
109
152
  main_with_args(project_id, project_root)
110
153
 
154
+ elif args.command == "code":
155
+ if not hasattr(args, "code_command") or not args.code_command:
156
+ code_parser.print_help()
157
+ sys.exit(1)
158
+
159
+ if args.code_command == "run":
160
+ from clerk.development.code_runner import main_with_args
161
+
162
+ main_with_args(project_root)
163
+
111
164
 
112
165
  if __name__ == "__main__":
113
166
  main()
@@ -0,0 +1,384 @@
1
+ """Code runner module for testing custom code with payloads."""
2
+ import json
3
+ import sys
4
+ from pathlib import Path
5
+ from typing import Any, Dict, Optional
6
+ from importlib import import_module
7
+ import importlib.util
8
+
9
+ from rich.console import Console
10
+ from rich.panel import Panel
11
+ from rich.prompt import Confirm, Prompt
12
+ from rich import print as rprint
13
+
14
+ console = Console()
15
+
16
+
17
+ def _generate_structured_data_code(structured_data_class) -> str:
18
+ """Generate code for StructuredData initialization with all fields.
19
+
20
+ Args:
21
+ structured_data_class: The StructuredData class from schema
22
+
23
+ Returns:
24
+ String with indented field assignments
25
+ """
26
+ from typing import get_origin, get_args
27
+ from pydantic import BaseModel
28
+
29
+ lines = []
30
+
31
+ # Get model fields
32
+ if hasattr(structured_data_class, 'model_fields'):
33
+ fields = structured_data_class.model_fields
34
+
35
+ for field_name, field_info in fields.items():
36
+ annotation = field_info.annotation
37
+
38
+ # Check if it's a List type
39
+ origin = get_origin(annotation)
40
+ if origin is list:
41
+ lines.append(f" {field_name}=[],")
42
+ # Check if it's an Optional type
43
+ elif origin is type(None) or (hasattr(annotation, '__origin__') and annotation.__origin__ is type(None)):
44
+ lines.append(f" {field_name}=None,")
45
+ # Check if the annotation is a BaseModel subclass
46
+ else:
47
+ # Try to check if it's a BaseModel (handle Optional types)
48
+ actual_type = annotation
49
+ if origin:
50
+ # For Optional[Type], get the actual type
51
+ args = get_args(annotation)
52
+ if args:
53
+ # Filter out NoneType
54
+ non_none_args = [arg for arg in args if arg is not type(None)]
55
+ if non_none_args:
56
+ actual_type = non_none_args[0]
57
+
58
+ # Check if actual_type is a class and subclass of BaseModel
59
+ try:
60
+ if isinstance(actual_type, type) and issubclass(actual_type, BaseModel):
61
+ class_name = actual_type.__name__
62
+ lines.append(f" {field_name}={class_name}(),")
63
+ else:
64
+ lines.append(f" {field_name}=None,")
65
+ except (TypeError, AttributeError):
66
+ lines.append(f" {field_name}=None,")
67
+
68
+ return "\n".join(lines)
69
+
70
+
71
+ def find_test_payloads(project_root: Path) -> list[Path]:
72
+ """Find all test payload Python files in test/payloads directory.
73
+
74
+ Args:
75
+ project_root: Project root directory
76
+
77
+ Returns:
78
+ List of Path objects for payload files
79
+ """
80
+ payload_dir = project_root / "test" / "payloads"
81
+
82
+ if not payload_dir.exists():
83
+ return []
84
+
85
+ # Find all .py files except __init__.py
86
+ return [p for p in payload_dir.glob("*.py") if p.name != "__init__.py"]
87
+
88
+
89
+ def create_test_payload_template(project_root: Path) -> Path:
90
+ """Create a template test payload Python file.
91
+
92
+ Args:
93
+ project_root: Project root directory
94
+
95
+ Returns:
96
+ Path to the created template file
97
+ """
98
+ payload_dir = project_root / "test" / "payloads"
99
+ payload_dir.mkdir(parents=True, exist_ok=True)
100
+
101
+ # Check if schema exists
102
+ schema_path = project_root / "src" / "schema.py"
103
+ if not schema_path.exists():
104
+ console.print("[red]x[/red] No schema found. Run 'clerk schema fetch' first.")
105
+ console.print("[dim]Cannot generate test payload without schema.[/dim]")
106
+ sys.exit(1)
107
+
108
+ console.print("[green]✓[/green] Found schema at src/schema.py")
109
+
110
+ # Load schema to generate structured data template
111
+ structured_data_code = None
112
+ try:
113
+ # Add src to path
114
+ src_path = str(project_root / "src")
115
+ if src_path not in sys.path:
116
+ sys.path.insert(0, src_path)
117
+
118
+ spec = importlib.util.spec_from_file_location("schema", schema_path)
119
+ if spec and spec.loader:
120
+ schema_module = importlib.util.module_from_spec(spec)
121
+ spec.loader.exec_module(schema_module)
122
+
123
+ if hasattr(schema_module, "StructuredData"):
124
+ structured_data_class = getattr(schema_module, "StructuredData")
125
+ # Generate code with all fields
126
+ structured_data_code = _generate_structured_data_code(structured_data_class)
127
+ except Exception as e:
128
+ console.print(f"[red]x[/red] Could not load schema: {str(e)}")
129
+ sys.exit(1)
130
+
131
+ if not structured_data_code:
132
+ console.print("[red]x[/red] Could not generate structured data code from schema.")
133
+ sys.exit(1)
134
+
135
+ # Get name from user
136
+ name = Prompt.ask(
137
+ "Enter a name for this test payload",
138
+ default="test_payload_1"
139
+ )
140
+
141
+ # Ensure .py extension
142
+ if not name.endswith(".py"):
143
+ name = f"{name}.py"
144
+
145
+ payload_path = payload_dir / name
146
+
147
+ # Load and populate template
148
+ template_dir = Path(__file__).parent / "templates"
149
+ template_path = template_dir / "test_payload.py.template"
150
+ template_code = template_path.read_text(encoding="utf-8")
151
+ # Replace placeholder with actual fields
152
+ template_code = template_code.replace("{structured_data_fields}", structured_data_code)
153
+
154
+ # Write the template
155
+ with open(payload_path, "w", encoding="utf-8") as f:
156
+ f.write(template_code)
157
+
158
+ console.print(f"\n[green]✓[/green] Created template payload: {payload_path}")
159
+ console.print("\n[yellow]Please edit this file to customize your test data before continuing.[/yellow]")
160
+
161
+ return payload_path
162
+
163
+
164
+ def select_payload(payloads: list[Path]) -> Path:
165
+ """Let user select a payload by number.
166
+
167
+ Args:
168
+ payloads: List of payload file paths
169
+
170
+ Returns:
171
+ Selected payload path
172
+ """
173
+ console.print("\n[bold]Available test payloads:[/bold]")
174
+ for i, payload in enumerate(payloads, 1):
175
+ console.print(f" [cyan]{i}[/cyan]. {payload.stem}")
176
+
177
+ while True:
178
+ try:
179
+ choice = Prompt.ask(
180
+ "\nSelect a payload",
181
+ default="1"
182
+ )
183
+ idx = int(choice) - 1
184
+ if 0 <= idx < len(payloads):
185
+ return payloads[idx]
186
+ else:
187
+ console.print(f"[red]Please enter a number between 1 and {len(payloads)}[/red]")
188
+ except ValueError:
189
+ console.print("[red]Please enter a valid number[/red]")
190
+
191
+
192
+ def load_payload(payload_path: Path, project_root: Path):
193
+ """Load payload from Python module.
194
+
195
+ Args:
196
+ payload_path: Path to payload Python file
197
+ project_root: Project root directory
198
+
199
+ Returns:
200
+ ClerkCodePayload object
201
+ """
202
+ # Add project root and src to path so imports work
203
+ project_root_str = str(project_root)
204
+ src_path = str(project_root / "src")
205
+
206
+ if project_root_str not in sys.path:
207
+ sys.path.insert(0, project_root_str)
208
+ if src_path not in sys.path:
209
+ sys.path.insert(0, src_path)
210
+
211
+ # Load the payload module
212
+ spec = importlib.util.spec_from_file_location(
213
+ f"test_payload_{payload_path.stem}",
214
+ payload_path
215
+ )
216
+ if not spec or not spec.loader:
217
+ raise ImportError(f"Could not load payload module from {payload_path}")
218
+
219
+ payload_module = importlib.util.module_from_spec(spec)
220
+ spec.loader.exec_module(payload_module)
221
+
222
+ # Get the payload object
223
+ if not hasattr(payload_module, "payload"):
224
+ raise AttributeError(f"Payload module must define a 'payload' variable")
225
+
226
+ return payload_module.payload
227
+
228
+
229
+ def run_main_with_payload(project_root: Path, payload_path: Path):
230
+ """Run main() from src/main.py with the selected payload.
231
+
232
+ Args:
233
+ project_root: Project root directory
234
+ payload_path: Path to the payload Python file
235
+ """
236
+ console.print()
237
+ console.print(Panel(
238
+ f"[bold]Running main() with payload: {payload_path.name}[/bold]",
239
+ style="cyan"
240
+ ))
241
+
242
+ # Load payload
243
+ try:
244
+ payload_obj = load_payload(payload_path, project_root)
245
+ console.print("[green]✓[/green] Loaded payload")
246
+ except Exception as e:
247
+ console.print(f"[red]x[/red] Failed to load payload: {str(e)}")
248
+ import traceback
249
+ console.print("[dim]" + traceback.format_exc() + "[/dim]")
250
+ sys.exit(1)
251
+
252
+ # Find main.py
253
+ main_path = project_root / "src" / "main.py"
254
+ if not main_path.exists():
255
+ console.print(f"[red]x[/red] main.py not found at {main_path}")
256
+ sys.exit(1)
257
+
258
+ # Add src to path
259
+ src_path = str(project_root / "src")
260
+ if src_path not in sys.path:
261
+ sys.path.insert(0, src_path)
262
+
263
+ # Start debugpy server and wait for VS Code to attach
264
+ import debugpy
265
+
266
+ debug_port = 5678
267
+
268
+ # Check if already running under debugger
269
+ if not debugpy.is_client_connected():
270
+ console.print(f"\n[cyan]Starting debug server on port {debug_port}...[/cyan]")
271
+ debugpy.listen(("localhost", debug_port))
272
+
273
+ console.print()
274
+ console.print("[bold yellow]⚡ Ready for debugging![/bold yellow]")
275
+ console.print()
276
+ console.print("[bold]To start debugging:[/bold]")
277
+ console.print(" [cyan]→ Press F5 in VS Code[/cyan]")
278
+ console.print(" [dim]or select 'Clerk: Debug Code Run' from the debug panel[/dim]")
279
+ console.print()
280
+ console.print("[dim]Press Ctrl+C to skip debugging and run without debugger[/dim]\n")
281
+
282
+ try:
283
+ debugpy.wait_for_client()
284
+ console.print("[green]✓[/green] Debugger attached!\n")
285
+ except KeyboardInterrupt:
286
+ console.print("\n[yellow]Skipping debugger, running without debug...[/yellow]\n")
287
+ else:
288
+ console.print("\n[green]✓[/green] Already running under debugger\n")
289
+
290
+ # Import and run
291
+ try:
292
+ console.print()
293
+ console.print("[bold cyan]═══════════════════════════════════════════════════════[/bold cyan]")
294
+ console.print("[bold cyan] Starting Execution [/bold cyan]")
295
+ console.print("[bold cyan]═══════════════════════════════════════════════════════[/bold cyan]")
296
+ console.print()
297
+
298
+ # Import main module
299
+ spec = importlib.util.spec_from_file_location("main", main_path)
300
+ if spec and spec.loader:
301
+ main_module = importlib.util.module_from_spec(spec)
302
+ spec.loader.exec_module(main_module)
303
+
304
+ # Call main
305
+ if hasattr(main_module, "main"):
306
+ # Run main with the loaded payload
307
+ result = main_module.main(payload_obj)
308
+
309
+ console.print()
310
+ console.print("[bold cyan]═══════════════════════════════════════════════════════[/bold cyan]")
311
+ console.print("[bold cyan] Execution Complete [/bold cyan]")
312
+ console.print("[bold cyan]═══════════════════════════════════════════════════════[/bold cyan]")
313
+ console.print()
314
+
315
+ # Show result
316
+ if result:
317
+ console.print("[bold]Result:[/bold]")
318
+ console.print(Panel(
319
+ f"Document ID: {result.document.id}\n"
320
+ f"Run ID: {result.run_id}",
321
+ title="Execution Result",
322
+ style="green"
323
+ ))
324
+
325
+ # Show updated structured_data
326
+ if result.structured_data:
327
+ console.print("\n[bold]Updated Structured Data:[/bold]")
328
+ rprint(result.structured_data)
329
+ else:
330
+ console.print("[yellow]![/yellow] No result returned")
331
+ else:
332
+ console.print(f"[red]x[/red] No main() function found in {main_path}")
333
+ sys.exit(1)
334
+ else:
335
+ console.print(f"[red]x[/red] Could not load {main_path}")
336
+ sys.exit(1)
337
+
338
+ except Exception as e:
339
+ console.print()
340
+ console.print(f"[red]x Error during execution:[/red] {str(e)}")
341
+ import traceback
342
+ console.print("[dim]" + traceback.format_exc() + "[/dim]")
343
+ sys.exit(1)
344
+
345
+
346
+ def main_with_args(project_root: Path):
347
+ """Main entry point for code runner.
348
+
349
+ Args:
350
+ project_root: Project root directory
351
+ """
352
+ console.print()
353
+ console.print(Panel(
354
+ "[bold]Clerk Code Runner[/bold]\n"
355
+ "Run your custom code with test payloads",
356
+ style="cyan"
357
+ ))
358
+
359
+ # Find payloads
360
+ payloads = find_test_payloads(project_root)
361
+
362
+ if not payloads:
363
+ console.print("\n[yellow]No test payloads found in test/payloads/[/yellow]")
364
+
365
+ if Confirm.ask("Would you like to generate a template payload?", default=True):
366
+ payload_path = create_test_payload_template(project_root)
367
+
368
+ console.print("\n[bold]Next steps:[/bold]")
369
+ console.print(f"1. Edit {payload_path} with your test data")
370
+ console.print("2. Run [cyan]clerk code run[/cyan] again to execute")
371
+ return
372
+ else:
373
+ console.print("\n[dim]Create a Python file in test/payloads/ and run again.[/dim]")
374
+ return
375
+
376
+ # Show available payloads
377
+ console.print(f"\n[green]✓[/green] Found {len(payloads)} test payload(s)")
378
+
379
+ # Let user select
380
+ selected_payload = select_payload(payloads)
381
+ console.print(f"\n[green]→[/green] Selected: {selected_payload.name}")
382
+
383
+ # Run with selected payload
384
+ run_main_with_payload(project_root, selected_payload)