pdd-cli 0.0.41__py3-none-any.whl → 0.0.43__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.

Potentially problematic release.


This version of pdd-cli might be problematic. Click here for more details.

pdd/sync_main.py ADDED
@@ -0,0 +1,333 @@
1
+ import re
2
+ import time
3
+ from pathlib import Path
4
+ from typing import Any, Dict, List, Tuple
5
+
6
+ import click
7
+ from rich.console import Console
8
+ from rich.panel import Panel
9
+ from rich.table import Table
10
+ from rich import print as rprint
11
+
12
+ # Relative imports from the pdd package
13
+ from . import DEFAULT_STRENGTH, DEFAULT_TIME
14
+ from .construct_paths import (
15
+ _is_known_language,
16
+ construct_paths,
17
+ _find_pddrc_file,
18
+ _load_pddrc_config,
19
+ _detect_context,
20
+ _get_context_config
21
+ )
22
+ from .sync_orchestration import sync_orchestration
23
+
24
+ # A simple regex for basename validation to prevent path traversal or other injection
25
+ VALID_BASENAME_CHARS = re.compile(r"^[a-zA-Z0-9_-]+$")
26
+
27
+
28
+ def _validate_basename(basename: str) -> None:
29
+ """Raises UsageError if the basename is invalid."""
30
+ if not basename:
31
+ raise click.UsageError("BASENAME cannot be empty.")
32
+ if not VALID_BASENAME_CHARS.match(basename):
33
+ raise click.UsageError(
34
+ f"Basename '{basename}' contains invalid characters. "
35
+ "Only alphanumeric, underscore, and hyphen are allowed."
36
+ )
37
+
38
+
39
+ def _detect_languages(basename: str, prompts_dir: Path) -> List[str]:
40
+ """
41
+ Detects all available languages for a given basename by finding
42
+ matching prompt files in the prompts directory.
43
+ Excludes runtime languages (LLM) as they cannot form valid development units.
44
+ """
45
+ development_languages = []
46
+ if not prompts_dir.is_dir():
47
+ return []
48
+
49
+ pattern = f"{basename}_*.prompt"
50
+ for prompt_file in prompts_dir.glob(pattern):
51
+ # stem is 'basename_language'
52
+ stem = prompt_file.stem
53
+ # Ensure the file starts with the exact basename followed by an underscore
54
+ if stem.startswith(f"{basename}_"):
55
+ potential_language = stem[len(basename) + 1 :]
56
+ try:
57
+ if _is_known_language(potential_language):
58
+ # Exclude runtime languages (LLM) as they cannot form valid development units
59
+ if potential_language.lower() != 'llm':
60
+ development_languages.append(potential_language)
61
+ except ValueError:
62
+ # PDD_PATH not set (likely during testing) - assume language is valid
63
+ # if it matches common language patterns
64
+ common_languages = {"python", "javascript", "java", "cpp", "c", "go", "rust", "typescript"}
65
+ if potential_language.lower() in common_languages:
66
+ development_languages.append(potential_language)
67
+ # Explicitly exclude 'llm' even in test scenarios
68
+
69
+ # Return only development languages, with Python prioritized first, then sorted alphabetically
70
+ if 'python' in development_languages:
71
+ # Put Python first, then the rest sorted alphabetically
72
+ other_languages = sorted([lang for lang in development_languages if lang != 'python'])
73
+ return ['python'] + other_languages
74
+ else:
75
+ # No Python, just return sorted alphabetically
76
+ return sorted(development_languages)
77
+
78
+
79
+ def sync_main(
80
+ ctx: click.Context,
81
+ basename: str,
82
+ max_attempts: int,
83
+ budget: float,
84
+ skip_verify: bool,
85
+ skip_tests: bool,
86
+ target_coverage: float,
87
+ log: bool,
88
+ ) -> Tuple[Dict[str, Any], float, str]:
89
+ """
90
+ CLI wrapper for the sync command. Handles parameter validation, path construction,
91
+ language detection, and orchestrates the sync workflow for each detected language.
92
+
93
+ Args:
94
+ ctx: The Click context object.
95
+ basename: The base name for the prompt file.
96
+ max_attempts: Maximum number of fix attempts.
97
+ budget: Maximum total cost for the sync process.
98
+ skip_verify: Skip the functional verification step.
99
+ skip_tests: Skip unit test generation and fixing.
100
+ target_coverage: Desired code coverage percentage.
101
+ log: If True, display sync logs instead of running the sync.
102
+
103
+ Returns:
104
+ A tuple containing the results dictionary, total cost, and primary model name.
105
+ """
106
+ console = Console()
107
+ start_time = time.time()
108
+
109
+ # 1. Retrieve global parameters from context
110
+ strength = ctx.obj.get("strength", DEFAULT_STRENGTH)
111
+ temperature = ctx.obj.get("temperature", 0.0)
112
+ time_param = ctx.obj.get("time", DEFAULT_TIME)
113
+ verbose = ctx.obj.get("verbose", False)
114
+ force = ctx.obj.get("force", False)
115
+ quiet = ctx.obj.get("quiet", False)
116
+ output_cost = ctx.obj.get("output_cost", None)
117
+ review_examples = ctx.obj.get("review_examples", False)
118
+ local = ctx.obj.get("local", False)
119
+ context_override = ctx.obj.get("context", None)
120
+
121
+ # 2. Validate inputs
122
+ _validate_basename(basename)
123
+ if budget <= 0:
124
+ raise click.BadParameter("Budget must be a positive number.", param_hint="--budget")
125
+ if max_attempts <= 0:
126
+ raise click.BadParameter("Max attempts must be a positive integer.", param_hint="--max-attempts")
127
+
128
+ if not quiet and budget < 1.0:
129
+ console.log(f"[yellow]Warning:[/] Budget of ${budget:.2f} is low. Complex operations may exceed this limit.")
130
+
131
+ # 3. Use construct_paths in 'discovery' mode to find the prompts directory.
132
+ try:
133
+ initial_config, _, _, _ = construct_paths(
134
+ input_file_paths={},
135
+ force=False,
136
+ quiet=True,
137
+ command="sync",
138
+ command_options={"basename": basename},
139
+ context_override=context_override,
140
+ )
141
+ prompts_dir = Path(initial_config.get("prompts_dir", "prompts"))
142
+ except Exception as e:
143
+ rprint(f"[bold red]Error initializing PDD paths:[/bold red] {e}")
144
+ raise click.Abort()
145
+
146
+ # 4. Detect all languages for the given basename
147
+ languages = _detect_languages(basename, prompts_dir)
148
+ if not languages:
149
+ raise click.UsageError(
150
+ f"No prompt files found for basename '{basename}' in directory '{prompts_dir}'.\n"
151
+ f"Expected files with format: '{basename}_<language>.prompt'"
152
+ )
153
+
154
+ # 5. Handle --log mode separately
155
+ if log:
156
+ if not quiet:
157
+ rprint(Panel(f"Displaying sync logs for [bold cyan]{basename}[/bold cyan]", title="PDD Sync Log", expand=False))
158
+
159
+ for lang in languages:
160
+ if not quiet:
161
+ rprint(f"\n--- Log for language: [bold green]{lang}[/bold green] ---")
162
+
163
+ # Use construct_paths to get proper directory configuration for log mode
164
+ prompt_file_path = prompts_dir / f"{basename}_{lang}.prompt"
165
+
166
+ try:
167
+ resolved_config, _, _, _ = construct_paths(
168
+ input_file_paths={"prompt_file": str(prompt_file_path)},
169
+ force=force,
170
+ quiet=True,
171
+ command="sync",
172
+ command_options={"basename": basename, "language": lang},
173
+ context_override=context_override,
174
+ )
175
+
176
+ code_dir = resolved_config.get("code_dir", "src")
177
+ tests_dir = resolved_config.get("tests_dir", "tests")
178
+ examples_dir = resolved_config.get("examples_dir", "examples")
179
+ except Exception:
180
+ # Fallback to default paths if construct_paths fails
181
+ code_dir = str(prompts_dir.parent / "src")
182
+ tests_dir = str(prompts_dir.parent / "tests")
183
+ examples_dir = str(prompts_dir.parent / "examples")
184
+
185
+ sync_orchestration(
186
+ basename=basename,
187
+ language=lang,
188
+ prompts_dir=str(prompts_dir),
189
+ code_dir=str(code_dir),
190
+ examples_dir=str(examples_dir),
191
+ tests_dir=str(tests_dir),
192
+ log=True,
193
+ verbose=verbose,
194
+ quiet=quiet,
195
+ )
196
+ return {}, 0.0, ""
197
+
198
+ # 6. Main Sync Workflow
199
+ if not quiet:
200
+ summary_panel = Panel(
201
+ f"Basename: [bold cyan]{basename}[/bold cyan]\n"
202
+ f"Languages: [bold green]{', '.join(languages)}[/bold green]\n"
203
+ f"Budget: [bold yellow]${budget:.2f}[/bold yellow]\n"
204
+ f"Max Attempts: [bold blue]{max_attempts}[/bold blue]",
205
+ title="PDD Sync Starting",
206
+ expand=False,
207
+ )
208
+ rprint(summary_panel)
209
+
210
+ aggregated_results: Dict[str, Any] = {"results_by_language": {}}
211
+ total_cost = 0.0
212
+ primary_model = ""
213
+ overall_success = True
214
+ remaining_budget = budget
215
+
216
+ for lang in languages:
217
+ if not quiet:
218
+ rprint(f"\n[bold]🚀 Syncing for language: [green]{lang}[/green]...[/bold]")
219
+
220
+ if remaining_budget <= 0:
221
+ if not quiet:
222
+ rprint(f"[yellow]Budget exhausted. Skipping sync for '{lang}'.[/yellow]")
223
+ overall_success = False
224
+ aggregated_results["results_by_language"][lang] = {"success": False, "error": "Budget exhausted"}
225
+ continue
226
+
227
+ try:
228
+ # Get the fully resolved configuration for this specific language using construct_paths.
229
+ prompt_file_path = prompts_dir / f"{basename}_{lang}.prompt"
230
+
231
+ command_options = {
232
+ "basename": basename,
233
+ "language": lang,
234
+ "max_attempts": max_attempts,
235
+ "budget": budget,
236
+ "target_coverage": target_coverage,
237
+ "strength": strength,
238
+ "temperature": temperature,
239
+ "time": time_param,
240
+ }
241
+
242
+ resolved_config, _, _, resolved_language = construct_paths(
243
+ input_file_paths={"prompt_file": str(prompt_file_path)},
244
+ force=force,
245
+ quiet=True,
246
+ command="sync",
247
+ command_options=command_options,
248
+ context_override=context_override,
249
+ )
250
+
251
+ # Extract all parameters directly from the resolved configuration
252
+ final_strength = resolved_config.get("strength", strength)
253
+ final_temp = resolved_config.get("temperature", temperature)
254
+ final_max_attempts = resolved_config.get("max_attempts", max_attempts)
255
+ final_target_coverage = resolved_config.get("target_coverage", target_coverage)
256
+
257
+ code_dir = resolved_config.get("code_dir", "src")
258
+ tests_dir = resolved_config.get("tests_dir", "tests")
259
+ examples_dir = resolved_config.get("examples_dir", "examples")
260
+
261
+ sync_result = sync_orchestration(
262
+ basename=basename,
263
+ language=resolved_language,
264
+ prompts_dir=str(prompts_dir),
265
+ code_dir=str(code_dir),
266
+ examples_dir=str(examples_dir),
267
+ tests_dir=str(tests_dir),
268
+ budget=remaining_budget,
269
+ max_attempts=final_max_attempts,
270
+ skip_verify=skip_verify,
271
+ skip_tests=skip_tests,
272
+ target_coverage=final_target_coverage,
273
+ strength=final_strength,
274
+ temperature=final_temp,
275
+ time_param=time_param,
276
+ force=force,
277
+ quiet=quiet,
278
+ verbose=verbose,
279
+ output_cost=output_cost,
280
+ review_examples=review_examples,
281
+ local=local,
282
+ context_config=resolved_config,
283
+ )
284
+
285
+ lang_cost = sync_result.get("total_cost", 0.0)
286
+ total_cost += lang_cost
287
+ remaining_budget -= lang_cost
288
+
289
+ if sync_result.get("model_name"):
290
+ primary_model = sync_result["model_name"]
291
+
292
+ if not sync_result.get("success", False):
293
+ overall_success = False
294
+
295
+ aggregated_results["results_by_language"][lang] = sync_result
296
+
297
+ except Exception as e:
298
+ if not quiet:
299
+ rprint(f"[bold red]An unexpected error occurred during sync for '{lang}':[/bold red] {e}")
300
+ if verbose:
301
+ console.print_exception(show_locals=True)
302
+ overall_success = False
303
+ aggregated_results["results_by_language"][lang] = {"success": False, "error": str(e)}
304
+
305
+ # 7. Final Summary Report
306
+ if not quiet:
307
+ elapsed_time = time.time() - start_time
308
+ final_table = Table(title="PDD Sync Complete", show_header=True, header_style="bold magenta")
309
+ final_table.add_column("Language", style="cyan", no_wrap=True)
310
+ final_table.add_column("Status", justify="center")
311
+ final_table.add_column("Cost (USD)", justify="right", style="yellow")
312
+ final_table.add_column("Details")
313
+
314
+ for lang, result in aggregated_results["results_by_language"].items():
315
+ status = "[green]Success[/green]" if result.get("success") else "[red]Failed[/red]"
316
+ cost_str = f"${result.get('total_cost', 0.0):.4f}"
317
+ details = result.get("summary") or result.get("error", "No details.")
318
+ final_table.add_row(lang, status, cost_str, str(details))
319
+
320
+ rprint(final_table)
321
+
322
+ summary_text = (
323
+ f"Total time: [bold]{elapsed_time:.2f}s[/bold] | "
324
+ f"Total cost: [bold yellow]${total_cost:.4f}[/bold yellow] | "
325
+ f"Overall status: {'[green]Success[/green]' if overall_success else '[red]Failed[/red]'}"
326
+ )
327
+ rprint(Panel(summary_text, expand=False))
328
+
329
+ aggregated_results["overall_success"] = overall_success
330
+ aggregated_results["total_cost"] = total_cost
331
+ aggregated_results["primary_model"] = primary_model
332
+
333
+ return aggregated_results, total_cost, primary_model