pdd-cli 0.0.46__py3-none-any.whl → 0.0.47__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/__init__.py +1 -1
- pdd/cli.py +42 -0
- pdd/cmd_test_main.py +5 -0
- pdd/construct_paths.py +16 -7
- pdd/fix_error_loop.py +36 -5
- pdd/llm_invoke.py +45 -7
- pdd/prompts/auto_include_LLM.prompt +51 -905
- pdd/summarize_directory.py +5 -0
- pdd/sync_determine_operation.py +163 -51
- pdd/sync_orchestration.py +213 -49
- pdd/update_model_costs.py +2 -2
- {pdd_cli-0.0.46.dist-info → pdd_cli-0.0.47.dist-info}/METADATA +3 -3
- {pdd_cli-0.0.46.dist-info → pdd_cli-0.0.47.dist-info}/RECORD +17 -17
- {pdd_cli-0.0.46.dist-info → pdd_cli-0.0.47.dist-info}/WHEEL +0 -0
- {pdd_cli-0.0.46.dist-info → pdd_cli-0.0.47.dist-info}/entry_points.txt +0 -0
- {pdd_cli-0.0.46.dist-info → pdd_cli-0.0.47.dist-info}/licenses/LICENSE +0 -0
- {pdd_cli-0.0.46.dist-info → pdd_cli-0.0.47.dist-info}/top_level.txt +0 -0
pdd/__init__.py
CHANGED
pdd/cli.py
CHANGED
|
@@ -34,6 +34,7 @@ from .fix_main import fix_main
|
|
|
34
34
|
from .fix_verification_main import fix_verification_main
|
|
35
35
|
from .install_completion import install_completion, get_local_pdd_path
|
|
36
36
|
from .preprocess_main import preprocess_main
|
|
37
|
+
from .pytest_output import run_pytest_and_capture_output
|
|
37
38
|
from .split_main import split_main
|
|
38
39
|
from .sync_main import sync_main
|
|
39
40
|
from .trace_main import trace_main
|
|
@@ -1176,6 +1177,47 @@ def sync(
|
|
|
1176
1177
|
return None
|
|
1177
1178
|
|
|
1178
1179
|
|
|
1180
|
+
@cli.command("pytest-output")
|
|
1181
|
+
@click.argument("test_file", type=click.Path(exists=True, dir_okay=False))
|
|
1182
|
+
@click.option(
|
|
1183
|
+
"--json-only",
|
|
1184
|
+
is_flag=True,
|
|
1185
|
+
default=False,
|
|
1186
|
+
help="Output only JSON to stdout for programmatic use.",
|
|
1187
|
+
)
|
|
1188
|
+
@click.pass_context
|
|
1189
|
+
# No @track_cost since this is a utility command
|
|
1190
|
+
def pytest_output_cmd(ctx: click.Context, test_file: str, json_only: bool) -> None:
|
|
1191
|
+
"""Run pytest on a test file and capture structured output.
|
|
1192
|
+
|
|
1193
|
+
This is a utility command used internally by PDD for capturing pytest results
|
|
1194
|
+
in a structured format. It can also be used directly for debugging test issues.
|
|
1195
|
+
|
|
1196
|
+
Examples:
|
|
1197
|
+
pdd pytest-output tests/test_example.py
|
|
1198
|
+
pdd pytest-output tests/test_example.py --json-only
|
|
1199
|
+
"""
|
|
1200
|
+
command_name = "pytest-output"
|
|
1201
|
+
quiet_mode = ctx.obj.get("quiet", False)
|
|
1202
|
+
|
|
1203
|
+
try:
|
|
1204
|
+
import json
|
|
1205
|
+
pytest_output = run_pytest_and_capture_output(test_file)
|
|
1206
|
+
|
|
1207
|
+
if json_only:
|
|
1208
|
+
# Print only valid JSON to stdout for programmatic use
|
|
1209
|
+
print(json.dumps(pytest_output))
|
|
1210
|
+
else:
|
|
1211
|
+
# Pretty print the output for interactive use
|
|
1212
|
+
if not quiet_mode:
|
|
1213
|
+
console.print(f"Running pytest on: [blue]{test_file}[/blue]")
|
|
1214
|
+
from rich.pretty import pprint
|
|
1215
|
+
pprint(pytest_output, console=console)
|
|
1216
|
+
|
|
1217
|
+
except Exception as e:
|
|
1218
|
+
handle_error(e, command_name, quiet_mode)
|
|
1219
|
+
|
|
1220
|
+
|
|
1179
1221
|
@cli.command("install_completion")
|
|
1180
1222
|
@click.pass_context
|
|
1181
1223
|
# No @track_cost
|
pdd/cmd_test_main.py
CHANGED
|
@@ -3,6 +3,7 @@ Main entry point for the 'test' command.
|
|
|
3
3
|
"""
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
import click
|
|
6
|
+
from pathlib import Path
|
|
6
7
|
# pylint: disable=redefined-builtin
|
|
7
8
|
from rich import print
|
|
8
9
|
|
|
@@ -165,6 +166,10 @@ def cmd_test_main(
|
|
|
165
166
|
return "", 0.0, ""
|
|
166
167
|
|
|
167
168
|
try:
|
|
169
|
+
# Ensure parent directory exists
|
|
170
|
+
output_path = Path(output_file)
|
|
171
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
172
|
+
|
|
168
173
|
with open(output_file, "w", encoding="utf-8") as file_handle:
|
|
169
174
|
file_handle.write(unit_test)
|
|
170
175
|
print(f"[bold green]Unit tests saved to:[/bold green] {output_file}")
|
pdd/construct_paths.py
CHANGED
|
@@ -497,11 +497,15 @@ def construct_paths(
|
|
|
497
497
|
for key, path_str in input_file_paths.items():
|
|
498
498
|
try:
|
|
499
499
|
path = Path(path_str).expanduser()
|
|
500
|
-
# Resolve non-error files strictly first
|
|
500
|
+
# Resolve non-error files strictly first, but be more lenient for sync command
|
|
501
501
|
if key != "error_file":
|
|
502
|
-
#
|
|
503
|
-
|
|
504
|
-
|
|
502
|
+
# For sync command, be more tolerant of non-existent files since we're just determining paths
|
|
503
|
+
if command == "sync":
|
|
504
|
+
input_paths[key] = path.resolve()
|
|
505
|
+
else:
|
|
506
|
+
# Let FileNotFoundError propagate naturally if path doesn't exist
|
|
507
|
+
resolved_path = path.resolve(strict=True)
|
|
508
|
+
input_paths[key] = resolved_path
|
|
505
509
|
else:
|
|
506
510
|
# Resolve error file non-strictly, existence checked later
|
|
507
511
|
input_paths[key] = path.resolve()
|
|
@@ -531,9 +535,14 @@ def construct_paths(
|
|
|
531
535
|
|
|
532
536
|
# Check existence again, especially for error_file which might have been created
|
|
533
537
|
if not path.exists():
|
|
534
|
-
#
|
|
535
|
-
|
|
536
|
-
|
|
538
|
+
# For sync command, be more tolerant of non-existent files since we're just determining paths
|
|
539
|
+
if command == "sync":
|
|
540
|
+
# Skip reading content for non-existent files in sync mode
|
|
541
|
+
continue
|
|
542
|
+
else:
|
|
543
|
+
# This case should ideally be caught by resolve(strict=True) earlier for non-error files
|
|
544
|
+
# Raise standard FileNotFoundError
|
|
545
|
+
raise FileNotFoundError(f"{path}")
|
|
537
546
|
|
|
538
547
|
if path.is_file(): # Read only if it's a file
|
|
539
548
|
try:
|
pdd/fix_error_loop.py
CHANGED
|
@@ -26,15 +26,46 @@ def run_pytest_on_file(test_file: str) -> tuple[int, int, int, str]:
|
|
|
26
26
|
Returns a tuple: (failures, errors, warnings, logs)
|
|
27
27
|
"""
|
|
28
28
|
try:
|
|
29
|
-
#
|
|
30
|
-
|
|
31
|
-
python_executable = detect_host_python_executable()
|
|
32
|
-
cmd = [python_executable, "-m", "pdd.pytest_output", "--json-only", test_file]
|
|
29
|
+
# Try using the pdd pytest-output command first (works with uv tool installs)
|
|
30
|
+
cmd = ["pdd", "pytest-output", "--json-only", test_file]
|
|
33
31
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
34
32
|
|
|
33
|
+
# If pdd command failed, try fallback approaches
|
|
34
|
+
if result.returncode != 0 and ("command not found" in result.stderr.lower() or "not found" in result.stderr.lower()):
|
|
35
|
+
# Fallback 1: Try direct function call (fastest for development)
|
|
36
|
+
try:
|
|
37
|
+
from .pytest_output import run_pytest_and_capture_output
|
|
38
|
+
pytest_output = run_pytest_and_capture_output(test_file)
|
|
39
|
+
result_stdout = json.dumps(pytest_output)
|
|
40
|
+
result = type('MockResult', (), {'stdout': result_stdout, 'stderr': '', 'returncode': 0})()
|
|
41
|
+
except ImportError:
|
|
42
|
+
# Fallback 2: Try python -m approach for development installs where pdd isn't in PATH
|
|
43
|
+
python_executable = detect_host_python_executable()
|
|
44
|
+
cmd = [python_executable, "-m", "pdd.pytest_output", "--json-only", test_file]
|
|
45
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
46
|
+
|
|
35
47
|
# Parse the JSON output from stdout
|
|
36
48
|
try:
|
|
37
|
-
|
|
49
|
+
# Extract just the JSON part from stdout (handles CLI contamination)
|
|
50
|
+
stdout_clean = result.stdout
|
|
51
|
+
json_start = stdout_clean.find('{')
|
|
52
|
+
if json_start == -1:
|
|
53
|
+
raise json.JSONDecodeError("No JSON found in output", stdout_clean, 0)
|
|
54
|
+
|
|
55
|
+
# Find the end of the JSON object by counting braces
|
|
56
|
+
brace_count = 0
|
|
57
|
+
json_end = json_start
|
|
58
|
+
for i, char in enumerate(stdout_clean[json_start:], json_start):
|
|
59
|
+
if char == '{':
|
|
60
|
+
brace_count += 1
|
|
61
|
+
elif char == '}':
|
|
62
|
+
brace_count -= 1
|
|
63
|
+
if brace_count == 0:
|
|
64
|
+
json_end = i + 1
|
|
65
|
+
break
|
|
66
|
+
|
|
67
|
+
json_str = stdout_clean[json_start:json_end]
|
|
68
|
+
output = json.loads(json_str)
|
|
38
69
|
test_results = output.get('test_results', [{}])[0]
|
|
39
70
|
|
|
40
71
|
# Check pytest's return code first
|
pdd/llm_invoke.py
CHANGED
|
@@ -5,6 +5,7 @@ import os
|
|
|
5
5
|
import pandas as pd
|
|
6
6
|
import litellm
|
|
7
7
|
import logging # ADDED FOR DETAILED LOGGING
|
|
8
|
+
import importlib.resources
|
|
8
9
|
|
|
9
10
|
# --- Configure Standard Python Logging ---
|
|
10
11
|
logger = logging.getLogger("pdd.llm_invoke")
|
|
@@ -190,12 +191,20 @@ ENV_PATH = PROJECT_ROOT / ".env"
|
|
|
190
191
|
user_pdd_dir = Path.home() / ".pdd"
|
|
191
192
|
user_model_csv_path = user_pdd_dir / "llm_model.csv"
|
|
192
193
|
|
|
194
|
+
# Check in order: user-specific, project-specific, package default
|
|
193
195
|
if user_model_csv_path.is_file():
|
|
194
196
|
LLM_MODEL_CSV_PATH = user_model_csv_path
|
|
195
197
|
logger.info(f"Using user-specific LLM model CSV: {LLM_MODEL_CSV_PATH}")
|
|
196
198
|
else:
|
|
197
|
-
|
|
198
|
-
|
|
199
|
+
# Check project-specific location (.pdd directory)
|
|
200
|
+
project_model_csv_path = PROJECT_ROOT / ".pdd" / "llm_model.csv"
|
|
201
|
+
if project_model_csv_path.is_file():
|
|
202
|
+
LLM_MODEL_CSV_PATH = project_model_csv_path
|
|
203
|
+
logger.info(f"Using project-specific LLM model CSV: {LLM_MODEL_CSV_PATH}")
|
|
204
|
+
else:
|
|
205
|
+
# Neither exists, we'll use a marker path that _load_model_data will handle
|
|
206
|
+
LLM_MODEL_CSV_PATH = None
|
|
207
|
+
logger.info("No local LLM model CSV found, will use package default")
|
|
199
208
|
# ---------------------------------
|
|
200
209
|
|
|
201
210
|
# Load environment variables from .env file
|
|
@@ -356,12 +365,41 @@ litellm.success_callback = [_litellm_success_callback]
|
|
|
356
365
|
|
|
357
366
|
# --- Helper Functions ---
|
|
358
367
|
|
|
359
|
-
def _load_model_data(csv_path: Path) -> pd.DataFrame:
|
|
360
|
-
"""Loads and preprocesses the LLM model data from CSV.
|
|
361
|
-
|
|
362
|
-
|
|
368
|
+
def _load_model_data(csv_path: Optional[Path]) -> pd.DataFrame:
|
|
369
|
+
"""Loads and preprocesses the LLM model data from CSV.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
csv_path: Path to CSV file, or None to use package default
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
DataFrame with model configuration data
|
|
376
|
+
"""
|
|
377
|
+
# If csv_path is provided, try to load from it
|
|
378
|
+
if csv_path is not None:
|
|
379
|
+
if not csv_path.exists():
|
|
380
|
+
logger.warning(f"Specified LLM model CSV not found at {csv_path}, trying package default")
|
|
381
|
+
csv_path = None
|
|
382
|
+
else:
|
|
383
|
+
try:
|
|
384
|
+
df = pd.read_csv(csv_path)
|
|
385
|
+
logger.debug(f"Loaded model data from {csv_path}")
|
|
386
|
+
# Continue with the rest of the function...
|
|
387
|
+
except Exception as e:
|
|
388
|
+
logger.warning(f"Failed to load CSV from {csv_path}: {e}, trying package default")
|
|
389
|
+
csv_path = None
|
|
390
|
+
|
|
391
|
+
# If csv_path is None or loading failed, use package default
|
|
392
|
+
if csv_path is None:
|
|
393
|
+
try:
|
|
394
|
+
# Use importlib.resources to load the packaged CSV
|
|
395
|
+
csv_data = importlib.resources.files('pdd').joinpath('data/llm_model.csv').read_text()
|
|
396
|
+
import io
|
|
397
|
+
df = pd.read_csv(io.StringIO(csv_data))
|
|
398
|
+
logger.info("Loaded model data from package default")
|
|
399
|
+
except Exception as e:
|
|
400
|
+
raise FileNotFoundError(f"Failed to load default LLM model CSV from package: {e}")
|
|
401
|
+
|
|
363
402
|
try:
|
|
364
|
-
df = pd.read_csv(csv_path)
|
|
365
403
|
# Basic validation and type conversion
|
|
366
404
|
required_cols = ['provider', 'model', 'input', 'output', 'coding_arena_elo', 'api_key', 'structured_output', 'reasoning_type']
|
|
367
405
|
for col in required_cols:
|