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.
- clerk/__init__.py +1 -1
- clerk/development/__init__.py +0 -0
- clerk/development/cli.py +113 -0
- clerk/development/gui/test_session.py +327 -0
- clerk/development/init_project.py +325 -0
- clerk/development/schema/fetch_schema.py +339 -0
- clerk/development/templates/exceptions.py.template +16 -0
- clerk/development/templates/main_basic.py.template +22 -0
- clerk/development/templates/main_gui.py.template +52 -0
- clerk/development/templates/rollbacks.py.template +17 -0
- clerk/development/templates/states.py.template +15 -0
- clerk/development/templates/transitions.py.template +26 -0
- clerk/gui_automation/requirements.txt +2 -0
- clerk/gui_automation/ui_actions/base.py +17 -1
- clerk/gui_automation/ui_state_machine/Readme.md +79 -0
- {clerk_sdk-0.4.17.dist-info → clerk_sdk-0.5.0.dist-info}/METADATA +13 -26
- {clerk_sdk-0.4.17.dist-info → clerk_sdk-0.5.0.dist-info}/RECORD +20 -7
- {clerk_sdk-0.4.17.dist-info → clerk_sdk-0.5.0.dist-info}/WHEEL +1 -2
- clerk_sdk-0.5.0.dist-info/entry_points.txt +2 -0
- clerk_sdk-0.4.17.dist-info/top_level.txt +0 -1
- {clerk_sdk-0.4.17.dist-info → clerk_sdk-0.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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()
|