clerk-sdk 0.4.17__py3-none-any.whl → 0.5.0__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.
@@ -0,0 +1,325 @@
1
+ """Project initialization module for Clerk custom code projects."""
2
+ import os
3
+ import sys
4
+ from pathlib import Path
5
+ from typing import Optional, Dict
6
+
7
+ from rich.console import Console
8
+ from rich.panel import Panel
9
+ from rich.prompt import Confirm, Prompt
10
+
11
+ console = Console()
12
+
13
+
14
+ def prompt_for_env_var(var_name: str, description: str, required: bool = True, default: str = "") -> str:
15
+ """Prompt user for an environment variable value."""
16
+ prompt_text = f"{var_name}"
17
+ if description:
18
+ prompt_text = f"{description} ({var_name})"
19
+
20
+ while True:
21
+ value = Prompt.ask(prompt_text, default=default if default else None)
22
+ if value or not required:
23
+ return value if value else ""
24
+ console.print(f"[yellow]{var_name} is required. Please provide a value.[/yellow]")
25
+
26
+
27
+ def create_env_file(gui_automation: bool = False) -> Dict[str, str]:
28
+ """Interactively create .env file with user secrets.
29
+
30
+ Args:
31
+ gui_automation: Whether GUI automation is enabled (adds REMOTE_DEVICE_NAME)
32
+
33
+ Returns:
34
+ Dictionary of environment variable key-value pairs
35
+ """
36
+ console.print()
37
+ console.print(Panel(
38
+ "[bold]Environment Configuration[/bold]\n"
39
+ "Please provide the following configuration values.",
40
+ style="cyan"
41
+ ))
42
+
43
+ env_vars = {
44
+ "CLERK_API_KEY": ("Clerk API Key", True, ""),
45
+ "PROJECT_ID": ("Project ID", True, ""),
46
+ }
47
+
48
+ # Add REMOTE_DEVICE_NAME if GUI automation is enabled
49
+ if gui_automation:
50
+ env_vars["REMOTE_DEVICE_NAME"] = ("Remote Device Name (for GUI automation)", True, "")
51
+
52
+ env_content = []
53
+ env_values = {}
54
+ env_path = Path(".env")
55
+
56
+ # Check if .env already exists
57
+ if env_path.exists():
58
+ if not Confirm.ask("\n[yellow].env file already exists. Overwrite?[/yellow]", default=False):
59
+ console.print("[dim]Using existing .env file[/dim]")
60
+ return load_env_file()
61
+
62
+ for var_name, (description, required, default) in env_vars.items():
63
+ value = prompt_for_env_var(var_name, description, required, default)
64
+ if value:
65
+ env_content.append(f"{var_name}={value}")
66
+ env_values[var_name] = value
67
+
68
+ # Write .env file
69
+ with open(env_path, 'w') as f:
70
+ f.write('\n'.join(env_content) + '\n')
71
+
72
+ console.print(f"\n[green]✓[/green] Created .env file with {len(env_content)} variables")
73
+ return env_values
74
+
75
+
76
+ def load_env_file() -> Dict[str, str]:
77
+ """Load environment variables from .env file.
78
+
79
+ Returns:
80
+ Dictionary of environment variable key-value pairs
81
+ """
82
+ env_values = {}
83
+ env_path = Path(".env")
84
+
85
+ if env_path.exists():
86
+ with open(env_path, 'r') as f:
87
+ for line in f:
88
+ line = line.strip()
89
+ if line and not line.startswith('#') and '=' in line:
90
+ key, value = line.split('=', 1)
91
+ env_values[key.strip()] = value.strip()
92
+
93
+ return env_values
94
+
95
+
96
+ def read_template(template_name: str) -> str:
97
+ """Read a template file from the templates directory."""
98
+ template_dir = Path(__file__).parent / "templates"
99
+ template_path = template_dir / template_name
100
+
101
+ if not template_path.exists():
102
+ raise FileNotFoundError(f"Template not found: {template_name}")
103
+
104
+ with open(template_path, 'r', encoding='utf-8') as f:
105
+ return f.read()
106
+
107
+
108
+ def create_main_py(target_dir: Path, with_gui: bool = False) -> None:
109
+ """Create main.py with or without GUI automation setup.
110
+
111
+ Args:
112
+ target_dir: Target directory where main.py should be created
113
+ with_gui: Whether to include GUI automation functionality
114
+ """
115
+ main_path = target_dir / "main.py"
116
+
117
+ if main_path.exists():
118
+ console.print(f"[yellow]![/yellow] {main_path} already exists, skipping...")
119
+ return
120
+
121
+ template_name = "main_gui.py.template" if with_gui else "main_basic.py.template"
122
+ content = read_template(template_name)
123
+
124
+ with open(main_path, "w", encoding='utf-8') as f:
125
+ f.write(content)
126
+
127
+ console.print(f"[green]+[/green] Created {main_path}")
128
+
129
+
130
+ def create_init_py(target_dir: Path) -> None:
131
+ """Create __init__.py in the target directory if it doesn't exist.
132
+
133
+ Args:
134
+ target_dir: Target directory where __init__.py should be created
135
+ """
136
+ init_path = target_dir / "__init__.py"
137
+
138
+ if init_path.exists():
139
+ console.print(f"[yellow]![/yellow] {init_path} already exists, skipping...")
140
+ return
141
+
142
+ with open(init_path, "w", encoding="utf-8") as f:
143
+ f.write("# Init file for the package\n")
144
+
145
+ console.print(f"[green]+[/green] Created {init_path}")
146
+
147
+
148
+ def create_gui_structure(target_dir: Path) -> None:
149
+ """Create GUI automation folder structure with template files.
150
+
151
+ Args:
152
+ target_dir: Target directory where gui folder should be created
153
+ """
154
+ console.print("\n[dim]Creating GUI automation structure...[/dim]")
155
+
156
+ gui_path = target_dir / "gui"
157
+ gui_path.mkdir(parents=True, exist_ok=True)
158
+
159
+ # Create targets subfolder
160
+ targets_path = gui_path / "targets"
161
+ targets_path.mkdir(exist_ok=True)
162
+
163
+ # Template files to create
164
+ template_files = [
165
+ "states.py.template",
166
+ "transitions.py.template",
167
+ "rollbacks.py.template",
168
+ "exceptions.py.template",
169
+ ]
170
+
171
+ for template_name in template_files:
172
+ output_name = template_name.replace(".template", "")
173
+ output_path = gui_path / output_name
174
+
175
+ if output_path.exists():
176
+ console.print(
177
+ f"[yellow]![/yellow] {output_path} already exists, skipping..."
178
+ )
179
+ continue
180
+
181
+ content = read_template(template_name)
182
+
183
+ with open(output_path, "w", encoding='utf-8') as f:
184
+ f.write(content)
185
+
186
+ console.print(f"[green]+[/green] Created GUI automation structure in {gui_path}")
187
+
188
+
189
+ def init_project(
190
+ target_dir: Optional[Path] = None,
191
+ with_gui: Optional[bool] = None
192
+ ) -> None:
193
+ """Initialize a new Clerk custom code project.
194
+
195
+ Args:
196
+ target_dir: Target directory for the project (defaults to ./src)
197
+ with_gui: Whether to include GUI automation functionality (prompts if None)
198
+ """
199
+ if target_dir is None:
200
+ target_dir = Path.cwd() / "src"
201
+
202
+ # Ensure target directory exists
203
+ target_dir.mkdir(parents=True, exist_ok=True)
204
+
205
+ # Welcome message
206
+ console.print(Panel(
207
+ "[bold cyan]Clerk Custom Code Setup[/bold cyan]\n\n"
208
+ "This will set up your Clerk custom code project.",
209
+ style="blue"
210
+ ))
211
+
212
+ # Prompt for GUI automation if not specified
213
+ if with_gui is None:
214
+ console.print()
215
+ with_gui = Confirm.ask(
216
+ "[cyan]Enable GUI automation functionality?[/cyan]",
217
+ default=False
218
+ )
219
+
220
+ gui_status = "ENABLED" if with_gui else "DISABLED"
221
+ console.print(f"\n[dim]GUI Automation: {gui_status}[/dim]")
222
+
223
+ # Create .env file and get environment variables
224
+ env_vars = create_env_file(gui_automation=with_gui)
225
+
226
+ if not env_vars:
227
+ console.print("\n[red]✗ Failed to configure environment[/red]")
228
+ sys.exit(1)
229
+
230
+ # Update os.environ with the new values for fetch_schema to use
231
+ for key, value in env_vars.items():
232
+ os.environ[key] = value
233
+
234
+ console.print("\n[bold]" + "=" * 60 + "[/bold]")
235
+ console.print("[bold cyan]Creating Project Structure[/bold cyan]")
236
+ console.print("[bold]" + "=" * 60 + "[/bold]")
237
+
238
+ # Create main.py
239
+ create_main_py(target_dir, with_gui=with_gui)
240
+
241
+ # Create __init__.py
242
+ create_init_py(target_dir)
243
+
244
+ # Create GUI automation structure if requested
245
+ if with_gui:
246
+ create_gui_structure(target_dir)
247
+
248
+ console.print("\n[bold]" + "=" * 60 + "[/bold]")
249
+ console.print("[bold cyan]Fetching Schema from Clerk[/bold cyan]")
250
+ console.print("[bold]" + "=" * 60 + "[/bold]")
251
+
252
+ # Fetch schema automatically
253
+ try:
254
+ from clerk.development.schema.fetch_schema import main_with_args as fetch_schema_main
255
+ project_id = env_vars.get("PROJECT_ID")
256
+ if project_id:
257
+ fetch_schema_main(project_id, Path.cwd())
258
+ else:
259
+ console.print("[yellow]⚠[/yellow] PROJECT_ID not found, skipping schema fetch")
260
+ except Exception as e:
261
+ console.print(f"[yellow]⚠[/yellow] Schema fetch failed: {e}")
262
+ console.print("[dim]You can run 'clerk fetch-schema' later to fetch the schema[/dim]")
263
+
264
+ # Final success message
265
+ console.print("\n[bold]" + "=" * 60 + "[/bold]")
266
+ console.print("[bold green]Setup Completed Successfully![/bold green]")
267
+ console.print("[bold]" + "=" * 60 + "[/bold]")
268
+
269
+ success_items = [
270
+ "Environment configured (.env created)",
271
+ "Schema fetched from Clerk",
272
+ ]
273
+
274
+ if with_gui:
275
+ success_items.insert(0, "GUI automation structure created")
276
+ success_items.insert(1, "main.py configured with ScreenPilot")
277
+ else:
278
+ success_items.insert(0, "Basic main.py created")
279
+
280
+ for item in success_items:
281
+ console.print(f"[green]✓[/green] {item}")
282
+
283
+ console.print("\n[cyan]Next steps:[/cyan]")
284
+ console.print(" 1. Start developing your custom code in src/main.py")
285
+ console.print(" 2. When ready, check README.md for deployment guidance.")
286
+ console.print("[bold]" + "=" * 60 + "[/bold]")
287
+
288
+
289
+ def main_with_args(gui_automation: Optional[bool] = None, target_dir: Optional[str] = None):
290
+ """Main entry point for CLI usage.
291
+
292
+ Args:
293
+ gui_automation: Whether to include GUI automation functionality (prompts if None)
294
+ target_dir: Target directory for the project
295
+ """
296
+ try:
297
+ target_path = Path(target_dir) if target_dir else None
298
+ init_project(target_dir=target_path, with_gui=gui_automation)
299
+ except KeyboardInterrupt:
300
+ console.print("\n\n[yellow]Setup cancelled by user[/yellow]")
301
+ sys.exit(1)
302
+ except Exception as e:
303
+ console.print(f"\n[red]✗ Error during project initialization: {e}[/red]")
304
+ sys.exit(1)
305
+
306
+
307
+ if __name__ == "__main__":
308
+ # For standalone testing
309
+ import argparse
310
+
311
+ parser = argparse.ArgumentParser(description="Initialize Clerk custom code project")
312
+ parser.add_argument(
313
+ "--gui-automation",
314
+ action="store_true",
315
+ help="Include GUI automation functionality"
316
+ )
317
+ parser.add_argument(
318
+ "--target-dir",
319
+ type=str,
320
+ default=None,
321
+ help="Target directory for the project (default: ./src)"
322
+ )
323
+
324
+ args = parser.parse_args()
325
+ main_with_args(gui_automation=args.gui_automation, target_dir=args.target_dir)
@@ -0,0 +1,339 @@
1
+ from pathlib import Path
2
+ from typing import Any, List, Optional, Dict, Tuple
3
+ from enum import Enum
4
+ from datetime import datetime
5
+ from pydantic import BaseModel, Field
6
+ import requests
7
+
8
+ from rich.console import Console
9
+
10
+ from clerk.client import Clerk
11
+ from clerk.exceptions.exceptions import ApplicationException
12
+
13
+ console = Console()
14
+
15
+
16
+ class VariableTypes(str, Enum):
17
+ STRING = "string"
18
+ NUMBER = "number"
19
+ DATE = "date"
20
+ BOOLEAN = "boolean"
21
+ DATETIME = "datetime"
22
+ TIME = "time"
23
+ OBJECT = "object"
24
+ ENUM = "enum"
25
+
26
+
27
+ class VariableData(BaseModel):
28
+ id: str
29
+ name: str
30
+ display_name: str
31
+ tags: List[str] = []
32
+ units: Optional[str] = None
33
+ description: Optional[str] = None
34
+ is_array: bool
35
+ parent_id: Optional[str] = None
36
+ type: VariableTypes
37
+ position_index: int
38
+ additional_properties: Optional[bool] = None
39
+ default: Any | None = None
40
+ enum_options: List[str] = Field(default_factory=list)
41
+
42
+
43
+ def fetch_schema(project_id: str) -> List[VariableData]:
44
+ """
45
+ Fetch schema from Clerk backend for a given project.
46
+
47
+ Args:
48
+ project_id: The project ID to fetch schema for
49
+
50
+ Returns:
51
+ List of VariableData objects
52
+
53
+ Raises:
54
+ ApplicationException: If the API key is invalid (401)
55
+ ApplicationException: If the project_id is invalid or not found (404)
56
+ ApplicationException: If there's another API error
57
+ """
58
+ try:
59
+ client = Clerk()
60
+ except ValueError as e:
61
+ raise ApplicationException(
62
+ message=f"Invalid or missing API key. Please set CLERK_API_KEY environment variable or provide it explicitly. Error: {str(e)}"
63
+ )
64
+
65
+ endpoint = "/schema"
66
+ params = {"project_id": project_id}
67
+
68
+ try:
69
+ res = client.get_request(endpoint=endpoint, params=params)
70
+ return [VariableData.model_validate(item) for item in res.data]
71
+ except requests.exceptions.HTTPError as e:
72
+ if e.response is not None:
73
+ status_code = e.response.status_code
74
+ if status_code == 401:
75
+ raise ApplicationException(
76
+ message="Invalid API key. Please check your CLERK_API_KEY."
77
+ )
78
+ elif status_code == 404:
79
+ raise ApplicationException(
80
+ message=f"Project not found. The project_id '{project_id}' does not exist or you don't have access to it."
81
+ )
82
+ elif status_code == 403:
83
+ raise ApplicationException(
84
+ message=f"Access forbidden. You don't have permission to access project '{project_id}'."
85
+ )
86
+ else:
87
+ raise ApplicationException(
88
+ message=f"API error (HTTP {status_code}): {e.response.text}"
89
+ )
90
+ else:
91
+ raise ApplicationException(message=f"HTTP error occurred: {str(e)}")
92
+ except requests.exceptions.RequestException as e:
93
+ raise ApplicationException(
94
+ message=f"Network error while fetching schema: {str(e)}"
95
+ )
96
+
97
+
98
+ def _python_type_from_variable(
99
+ var: VariableData, nested_models: Dict[str, str]
100
+ ) -> Tuple[str, bool]:
101
+ """Convert VariableData type to Python type string
102
+
103
+ Returns:
104
+ tuple of (type_string, is_leaf_value)
105
+ is_leaf_value is True for primitive types, False for lists and BaseModels
106
+ """
107
+ type_map = {
108
+ VariableTypes.STRING: "str",
109
+ VariableTypes.NUMBER: "float",
110
+ VariableTypes.DATE: "date",
111
+ VariableTypes.DATETIME: "datetime",
112
+ VariableTypes.TIME: "time",
113
+ VariableTypes.BOOLEAN: "bool",
114
+ VariableTypes.ENUM: "str", # Will be refined with Literal if enum_options exist
115
+ }
116
+
117
+ is_leaf = True # Assume leaf unless it's a list or object
118
+
119
+ if var.type == VariableTypes.OBJECT:
120
+ # Use the nested model class name
121
+ base_type = nested_models.get(var.id, "Dict[str, Any]")
122
+ is_leaf = False # Objects are not leaf values
123
+ elif var.type == VariableTypes.ENUM and var.enum_options:
124
+ # Create Literal type for enum
125
+ options = ", ".join([f'"{opt}"' for opt in var.enum_options])
126
+ base_type = f"Literal[{options}]"
127
+ else:
128
+ base_type = type_map.get(var.type, "Any")
129
+
130
+ # Handle arrays
131
+ if var.is_array:
132
+ is_leaf = False # Lists are not leaf values
133
+ return f"List[{base_type}]", is_leaf
134
+
135
+ return base_type, is_leaf
136
+
137
+
138
+ def generate_models_from_schema(
139
+ variables: List[VariableData], output_file: Optional[Path] = None
140
+ ) -> str:
141
+ """
142
+ Generate Pydantic BaseModel classes from schema variables.
143
+
144
+ Args:
145
+ variables: List of VariableData objects
146
+ output_file: Optional path to write the generated code
147
+
148
+ Returns:
149
+ Generated Python code as string
150
+ """
151
+ # Group variables by parent_id
152
+ root_vars: List[VariableData] = []
153
+ nested_vars: Dict[str, List[VariableData]] = {}
154
+
155
+ for var in sorted(variables, key=lambda v: v.position_index):
156
+ if var.parent_id is None:
157
+ root_vars.append(var)
158
+ else:
159
+ if var.parent_id not in nested_vars:
160
+ nested_vars[var.parent_id] = []
161
+ nested_vars[var.parent_id].append(var)
162
+
163
+ # Map variable IDs to their generated class names using name field
164
+ nested_models: Dict[str, str] = {}
165
+ var_id_to_data: Dict[str, VariableData] = {var.id: var for var in variables}
166
+
167
+ for parent_id in nested_vars.keys():
168
+ parent_var = var_id_to_data.get(parent_id)
169
+ if parent_var and parent_var.name:
170
+ # Use name field and convert snake_case to PascalCase
171
+ class_name = "".join(
172
+ word.capitalize() for word in parent_var.name.split("_")
173
+ )
174
+ else:
175
+ # Fallback to parent_id
176
+ class_name = "".join(word.capitalize() for word in parent_id.split("_"))
177
+ nested_models[parent_id] = class_name
178
+
179
+ code_lines: List[str] = []
180
+
181
+ # Autogenerated code comment with sync timestamp
182
+ sync_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
183
+ code_lines.append(
184
+ f"# Autogenerated by the fetch_schema tool - do not edit manually."
185
+ )
186
+ code_lines.append(f"# Last fetched: {sync_timestamp}\n")
187
+
188
+ # Generate imports
189
+ imports = [
190
+ "from typing import Any, List, Optional, Dict",
191
+ "from datetime import date, datetime, time",
192
+ "from pydantic import BaseModel, Field",
193
+ ]
194
+
195
+ # Check if we need Literal
196
+ has_enums = any(var.type == VariableTypes.ENUM and var.enum_options for var in variables)
197
+ if has_enums:
198
+ imports[0] = "from typing import Any, List, Optional, Dict, Literal"
199
+
200
+ code_lines.extend(imports)
201
+ code_lines.append("")
202
+
203
+ # Generate nested models first (bottom-up)
204
+ generated_classes = set()
205
+
206
+ def generate_class(var_id: str, vars_list: List[VariableData], class_name: str):
207
+ if class_name in generated_classes:
208
+ return
209
+
210
+ # First generate any nested children
211
+ for var in vars_list:
212
+ if var.type == VariableTypes.OBJECT and var.id in nested_vars:
213
+ child_class_name = nested_models[var.id]
214
+ generate_class(var.id, nested_vars[var.id], child_class_name)
215
+
216
+ # Generate this class
217
+ code_lines.append(f"class {class_name}(BaseModel):")
218
+
219
+ if not vars_list:
220
+ code_lines.append(" pass")
221
+ else:
222
+ for var in sorted(vars_list, key=lambda v: v.position_index):
223
+ field_name = var.name
224
+ python_type, is_leaf = _python_type_from_variable(var, nested_models)
225
+
226
+ # Make leaf values Optional and default to None
227
+ if is_leaf:
228
+ python_type = f"Optional[{python_type}]"
229
+
230
+ # Build field definition
231
+ field_parts = []
232
+ if var.description:
233
+ # Escape double quotes and newlines in description
234
+ escaped_desc = (
235
+ var.description.replace('"', '\\"')
236
+ .replace("\n", "\\n")
237
+ .replace("\r", "")
238
+ )
239
+ field_parts.append(f'description="{escaped_desc}"')
240
+
241
+ # Set default to None for leaf values, or use existing default
242
+ if is_leaf:
243
+ if var.default is not None:
244
+ field_parts.append(f"default={repr(var.default)}")
245
+ else:
246
+ field_parts.append("default=None")
247
+ elif var.default is not None:
248
+ field_parts.append(f"default={repr(var.default)}")
249
+
250
+ if field_parts:
251
+ field_def = f"Field({', '.join(field_parts)})"
252
+ code_lines.append(f" {field_name}: {python_type} = {field_def}")
253
+ else:
254
+ code_lines.append(f" {field_name}: {python_type}")
255
+
256
+ code_lines.append("")
257
+ generated_classes.add(class_name)
258
+
259
+ # Generate all nested models
260
+ for var_id, vars_list in nested_vars.items():
261
+ class_name = nested_models[var_id]
262
+ generate_class(var_id, vars_list, class_name)
263
+
264
+ # Generate root model
265
+ code_lines.append("class StructuredData(BaseModel):")
266
+ if not root_vars:
267
+ code_lines.append(" pass")
268
+ else:
269
+ for var in sorted(root_vars, key=lambda v: v.position_index):
270
+ field_name = var.name
271
+ python_type, is_leaf = _python_type_from_variable(var, nested_models)
272
+
273
+ # Make leaf values Optional and default to None
274
+ if is_leaf:
275
+ python_type = f"Optional[{python_type}]"
276
+
277
+ # Build field definition
278
+ field_parts = []
279
+ if var.description:
280
+ # Escape double quotes and newlines in description
281
+ escaped_desc = (
282
+ var.description.replace('"', '\\"')
283
+ .replace("\n", "\\n")
284
+ .replace("\r", "")
285
+ )
286
+ field_parts.append(f'description="{escaped_desc}"')
287
+
288
+ # Set default to None for leaf values, or use existing default
289
+ if is_leaf:
290
+ if var.default is not None:
291
+ field_parts.append(f"default={repr(var.default)}")
292
+ else:
293
+ field_parts.append("default=None")
294
+ elif var.default is not None:
295
+ field_parts.append(f"default={repr(var.default)}")
296
+
297
+ if field_parts:
298
+ field_def = f"Field({', '.join(field_parts)})"
299
+ code_lines.append(f" {field_name}: {python_type} = {field_def}")
300
+ else:
301
+ code_lines.append(f" {field_name}: {python_type}")
302
+
303
+ generated_code = "\n".join(code_lines)
304
+
305
+ # Write to file if specified
306
+ if output_file:
307
+ output_file.parent.mkdir(parents=True, exist_ok=True)
308
+ output_file.write_text(generated_code)
309
+
310
+ return generated_code
311
+
312
+
313
+ def main_with_args(project_id: str, project_root: Path | None = None):
314
+ """Main logic that can be called from CLI or programmatically"""
315
+ try:
316
+ with console.status(
317
+ f"[dim]Fetching schema for project: {project_id}...", spinner="dots"
318
+ ):
319
+ variables = fetch_schema(project_id)
320
+
321
+ console.print(f"[green]+[/green] Found {len(variables)} variables")
322
+
323
+ # Always save to schema.py in project root
324
+ if project_root is None:
325
+ project_root = Path.cwd()
326
+ output_file = project_root / "src" / "schema.py"
327
+
328
+ with console.status("[dim]Generating Pydantic models...", spinner="dots"):
329
+ generate_models_from_schema(variables, output_file)
330
+
331
+ console.print(
332
+ f"[green]+[/green] Schema generated and written to: {output_file}"
333
+ )
334
+ except ApplicationException as e:
335
+ console.print(f"[red]x Error: {e.message}[/red]")
336
+ raise
337
+ except Exception as e:
338
+ console.print(f"[red]x Unexpected error: {str(e)}[/red]")
339
+ raise
@@ -0,0 +1,16 @@
1
+ """
2
+ # Instructions
3
+
4
+ Define custom exceptions for ScreenPilot here.
5
+ - Any exception will be caught by ScreenPilot's main loop, activating rollback mode and course correction.
6
+ - Raise subclasses of BusinessException to indicate business process conditions that should not trigger rollbacks.
7
+
8
+ # Example
9
+ ```python
10
+ class CustomError(BusinessException):
11
+ pass
12
+ ```
13
+
14
+ """
15
+
16
+ from clerk.gui_automation.ui_state_machine.exceptions import BusinessException
@@ -0,0 +1,22 @@
1
+ from clerk.decorator import clerk_code
2
+ from clerk.decorator.models import ClerkCodePayload
3
+ from clerk.utils import logger
4
+
5
+ from src.schema import StructuredData
6
+
7
+ @clerk_code()
8
+ def main(payload: ClerkCodePayload):
9
+ """Main program"""
10
+
11
+ data = StructuredData.model_validate(payload.structured_data)
12
+ logger.info("Custom code started")
13
+
14
+ # TODO: Implement your custom code logic here
15
+
16
+ logger.info("Custom code completed")
17
+ payload.structured_data = data.model_dump()
18
+ return payload
19
+
20
+
21
+ if __name__ == "__main__":
22
+ main()