refactor default artifacts workspace and path compatibility
This commit is contained in:
+102
-39
@@ -12,6 +12,7 @@ from typing import Optional
|
||||
|
||||
import click
|
||||
import yaml
|
||||
from click.core import ParameterSource
|
||||
|
||||
from vlm.config import Config, load_config, create_default_config, validate_config
|
||||
from vlm.context import CLIContext, pass_context
|
||||
@@ -24,6 +25,57 @@ def default_config_path() -> Path:
|
||||
return Path.home() / ".vlm" / "config.yaml"
|
||||
|
||||
|
||||
def _workspace_dir_from_context() -> Path:
|
||||
"""Resolve workspace directory from CLI context at runtime."""
|
||||
click_ctx = click.get_current_context(silent=True)
|
||||
if click_ctx and isinstance(click_ctx.obj, CLIContext):
|
||||
return click_ctx.obj.config.workspace_dir
|
||||
return Path("artifacts")
|
||||
|
||||
|
||||
def default_artifact_path(filename: str) -> Path:
|
||||
"""Build default artifact path for a filename at runtime."""
|
||||
return _workspace_dir_from_context() / filename
|
||||
|
||||
|
||||
def _is_default_parameter(parameter_name: str) -> bool:
|
||||
"""Check whether a parameter value came from Click default."""
|
||||
click_ctx = click.get_current_context(silent=True)
|
||||
if click_ctx is None:
|
||||
return False
|
||||
return click_ctx.get_parameter_source(parameter_name) == ParameterSource.DEFAULT
|
||||
|
||||
|
||||
def resolve_legacy_default_input_path(
|
||||
current_path: Path,
|
||||
parameter_name: str,
|
||||
legacy_filename: str,
|
||||
option_name: str,
|
||||
) -> Path:
|
||||
"""Fallback to legacy root path when default workspace file is missing.
|
||||
|
||||
This keeps stage-A compatibility for users who still have legacy artifacts
|
||||
in repository root while printing a migration warning.
|
||||
"""
|
||||
if not _is_default_parameter(parameter_name):
|
||||
return current_path
|
||||
|
||||
workspace_default = default_artifact_path(legacy_filename)
|
||||
legacy_default = Path(legacy_filename)
|
||||
if current_path != workspace_default:
|
||||
return current_path
|
||||
if current_path.exists() or not legacy_default.exists():
|
||||
return current_path
|
||||
|
||||
click.echo(
|
||||
"Warning: detected legacy default input at "
|
||||
f"{legacy_default.resolve()}. Please migrate to "
|
||||
f"{workspace_default.resolve()} (example: {option_name} {workspace_default.resolve()}).",
|
||||
err=True,
|
||||
)
|
||||
return legacy_default
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.option(
|
||||
'--config',
|
||||
@@ -107,8 +159,8 @@ def main(ctx, config: Path, log_level: Optional[str]):
|
||||
@click.option(
|
||||
'--output',
|
||||
type=click.Path(path_type=Path),
|
||||
default=Path('inventory.csv'),
|
||||
help='Output file for inventory (default: inventory.csv)'
|
||||
default=lambda: default_artifact_path('inventory.csv'),
|
||||
help='Output file for inventory (default: artifacts/inventory.csv)'
|
||||
)
|
||||
@click.option(
|
||||
'--metadata/--no-metadata',
|
||||
@@ -160,15 +212,15 @@ def scan(
|
||||
@main.command()
|
||||
@click.option(
|
||||
'--input',
|
||||
type=click.Path(exists=True, path_type=Path),
|
||||
default=Path('inventory.csv'),
|
||||
help='Input inventory CSV file (default: inventory.csv)'
|
||||
type=click.Path(path_type=Path),
|
||||
default=lambda: default_artifact_path('inventory.csv'),
|
||||
help='Input inventory CSV file (default: artifacts/inventory.csv)'
|
||||
)
|
||||
@click.option(
|
||||
'--output',
|
||||
type=click.Path(path_type=Path),
|
||||
default=Path('identities.json'),
|
||||
help='Output file for parsed identities (default: identities.json)'
|
||||
default=lambda: default_artifact_path('identities.json'),
|
||||
help='Output file for parsed identities (default: artifacts/identities.json)'
|
||||
)
|
||||
@click.option(
|
||||
'--inventory',
|
||||
@@ -194,6 +246,7 @@ def parse(ctx: CLIContext, input: Path, output: Path, inventory: Optional[Path])
|
||||
vlm parse --output parsed_identities.json # Custom output
|
||||
"""
|
||||
try:
|
||||
input = resolve_legacy_default_input_path(input, "input", "inventory.csv", "--input")
|
||||
from vlm.commands.parse import parse_cmd
|
||||
parse_cmd(ctx, input, output, inventory)
|
||||
except FileNotFoundError:
|
||||
@@ -213,9 +266,9 @@ def parse(ctx: CLIContext, input: Path, output: Path, inventory: Optional[Path])
|
||||
@main.command()
|
||||
@click.option(
|
||||
'--input',
|
||||
type=click.Path(exists=True, path_type=Path),
|
||||
default=Path('identities.json'),
|
||||
help='Path to identities JSON file (default: identities.json)'
|
||||
type=click.Path(path_type=Path),
|
||||
default=lambda: default_artifact_path('identities.json'),
|
||||
help='Path to identities JSON file (default: artifacts/identities.json)'
|
||||
)
|
||||
@click.option(
|
||||
'--output',
|
||||
@@ -265,6 +318,7 @@ def enrich(
|
||||
results back into identities JSON.
|
||||
"""
|
||||
try:
|
||||
input = resolve_legacy_default_input_path(input, "input", "identities.json", "--input")
|
||||
from vlm.commands.enrich import enrich_cmd
|
||||
enrich_cmd(
|
||||
ctx,
|
||||
@@ -296,15 +350,15 @@ def enrich(
|
||||
@main.command()
|
||||
@click.option(
|
||||
'--input',
|
||||
type=click.Path(exists=True, path_type=Path),
|
||||
default=Path('identities.json'),
|
||||
help='Path to parsed identities JSON file (default: identities.json)'
|
||||
type=click.Path(path_type=Path),
|
||||
default=lambda: default_artifact_path('identities.json'),
|
||||
help='Path to parsed identities JSON file (default: artifacts/identities.json)'
|
||||
)
|
||||
@click.option(
|
||||
'--output',
|
||||
type=click.Path(path_type=Path),
|
||||
default=Path('analysis.json'),
|
||||
help='Path to save analysis results (default: analysis.json)'
|
||||
default=lambda: default_artifact_path('analysis.json'),
|
||||
help='Path to save analysis results (default: artifacts/analysis.json)'
|
||||
)
|
||||
@click.option(
|
||||
'--inventory',
|
||||
@@ -327,6 +381,7 @@ def analyze(ctx: CLIContext, input: Path, output: Path, inventory: Optional[Path
|
||||
vlm analyze --output my_analysis.json # Custom output
|
||||
"""
|
||||
try:
|
||||
input = resolve_legacy_default_input_path(input, "input", "identities.json", "--input")
|
||||
from vlm.commands.analyze import analyze_cmd
|
||||
analyze_cmd(ctx, input, output, inventory)
|
||||
except FileNotFoundError:
|
||||
@@ -346,15 +401,15 @@ def analyze(ctx: CLIContext, input: Path, output: Path, inventory: Optional[Path
|
||||
@main.command()
|
||||
@click.option(
|
||||
'--input',
|
||||
type=click.Path(exists=True, path_type=Path),
|
||||
default=Path('identities.json'),
|
||||
help='Path to parsed identities JSON file (default: identities.json)'
|
||||
type=click.Path(path_type=Path),
|
||||
default=lambda: default_artifact_path('identities.json'),
|
||||
help='Path to parsed identities JSON file (default: artifacts/identities.json)'
|
||||
)
|
||||
@click.option(
|
||||
'--output',
|
||||
type=click.Path(path_type=Path),
|
||||
default=Path('plan.json'),
|
||||
help='Path to save execution plan (default: plan.json)'
|
||||
default=lambda: default_artifact_path('plan.json'),
|
||||
help='Path to save execution plan (default: artifacts/plan.json)'
|
||||
)
|
||||
@click.option(
|
||||
'--analysis',
|
||||
@@ -377,6 +432,7 @@ def plan(ctx: CLIContext, input: Path, output: Path, analysis: Path | None):
|
||||
vlm plan --output my_plan.json # Custom output
|
||||
"""
|
||||
try:
|
||||
input = resolve_legacy_default_input_path(input, "input", "identities.json", "--input")
|
||||
from vlm.commands.plan import plan_cmd
|
||||
plan_cmd(ctx, input, output, analysis)
|
||||
except FileNotFoundError:
|
||||
@@ -397,14 +453,14 @@ def plan(ctx: CLIContext, input: Path, output: Path, analysis: Path | None):
|
||||
@click.option(
|
||||
'--input',
|
||||
type=click.Path(exists=True, path_type=Path),
|
||||
default=Path('plan.json'),
|
||||
help='Path to execution plan JSON file (default: plan.json)'
|
||||
default=lambda: default_artifact_path('plan.json'),
|
||||
help='Path to execution plan JSON file (default: artifacts/plan.json)'
|
||||
)
|
||||
@click.option(
|
||||
'--output',
|
||||
type=click.Path(path_type=Path),
|
||||
default=Path('plan_manual_review.csv'),
|
||||
help='Path to save manual review CSV (default: plan_manual_review.csv)'
|
||||
default=lambda: default_artifact_path('plan_manual_review.csv'),
|
||||
help='Path to save manual review CSV (default: artifacts/plan_manual_review.csv)'
|
||||
)
|
||||
@click.option(
|
||||
'--season-threshold',
|
||||
@@ -435,6 +491,7 @@ def review_plan_cmd(
|
||||
logger = ctx.logger
|
||||
|
||||
try:
|
||||
input = resolve_legacy_default_input_path(input, "input", "plan.json", "--input")
|
||||
if season_threshold < 1 or episode_threshold < 1:
|
||||
click.echo("Error: thresholds must be >= 1", err=True)
|
||||
sys.exit(1)
|
||||
@@ -491,9 +548,9 @@ def review_plan_cmd(
|
||||
@main.command()
|
||||
@click.option(
|
||||
'--plan',
|
||||
type=click.Path(exists=True, path_type=Path),
|
||||
default=Path('plan.json'),
|
||||
help='Path to execution plan JSON file (default: plan.json)'
|
||||
type=click.Path(path_type=Path),
|
||||
default=lambda: default_artifact_path('plan.json'),
|
||||
help='Path to execution plan JSON file (default: artifacts/plan.json)'
|
||||
)
|
||||
@click.option(
|
||||
'--confirm',
|
||||
@@ -541,6 +598,7 @@ def execute(ctx: CLIContext, plan: Path, confirm: bool, yes: bool, verbose_ops:
|
||||
vlm execute --confirm --yes # Execute without confirmation prompt
|
||||
"""
|
||||
try:
|
||||
plan = resolve_legacy_default_input_path(plan, "plan", "plan.json", "--plan")
|
||||
from vlm.commands.execute import execute_cmd
|
||||
execute_cmd(ctx, plan, confirm, yes, verbose_ops, preserve_directories, safe_mode)
|
||||
except FileNotFoundError:
|
||||
@@ -827,9 +885,9 @@ def report(ctx: CLIContext):
|
||||
)
|
||||
@click.option(
|
||||
'--input',
|
||||
type=click.Path(exists=True, path_type=Path),
|
||||
default=Path('inventory.csv'),
|
||||
help='Input inventory CSV file (default: inventory.csv)'
|
||||
type=click.Path(path_type=Path),
|
||||
default=lambda: default_artifact_path('inventory.csv'),
|
||||
help='Input inventory CSV file (default: artifacts/inventory.csv)'
|
||||
)
|
||||
@click.option(
|
||||
'--output',
|
||||
@@ -856,6 +914,7 @@ def report_inventory(ctx: CLIContext, format: str, input: Path, output: Optional
|
||||
logger = ctx.logger
|
||||
|
||||
try:
|
||||
input = resolve_legacy_default_input_path(input, "input", "inventory.csv", "--input")
|
||||
# Load inventory from CSV
|
||||
click.echo(f"Loading inventory from: {input}")
|
||||
|
||||
@@ -905,9 +964,9 @@ def report_inventory(ctx: CLIContext, format: str, input: Path, output: Optional
|
||||
)
|
||||
@click.option(
|
||||
'--input',
|
||||
type=click.Path(exists=True, path_type=Path),
|
||||
default=Path('analysis.json'),
|
||||
help='Input analysis JSON file (default: analysis.json)'
|
||||
type=click.Path(path_type=Path),
|
||||
default=lambda: default_artifact_path('analysis.json'),
|
||||
help='Input analysis JSON file (default: artifacts/analysis.json)'
|
||||
)
|
||||
@click.option(
|
||||
'--output',
|
||||
@@ -942,6 +1001,7 @@ def report_completeness(ctx: CLIContext, format: str, input: Path, output: Optio
|
||||
config = ctx.config
|
||||
logger = ctx.logger
|
||||
|
||||
input = resolve_legacy_default_input_path(input, "input", "analysis.json", "--input")
|
||||
plan_summary = None
|
||||
if plan:
|
||||
if not plan.exists():
|
||||
@@ -1017,9 +1077,9 @@ def report_completeness(ctx: CLIContext, format: str, input: Path, output: Optio
|
||||
)
|
||||
@click.option(
|
||||
'--input',
|
||||
type=click.Path(exists=True, path_type=Path),
|
||||
default=Path('analysis.json'),
|
||||
help='Input analysis JSON file (default: analysis.json)'
|
||||
type=click.Path(path_type=Path),
|
||||
default=lambda: default_artifact_path('analysis.json'),
|
||||
help='Input analysis JSON file (default: artifacts/analysis.json)'
|
||||
)
|
||||
@click.option(
|
||||
'--output',
|
||||
@@ -1056,6 +1116,7 @@ def report_duplicates(ctx: CLIContext, format: str, input: Path, output: Optiona
|
||||
config = ctx.config
|
||||
logger = ctx.logger
|
||||
|
||||
input = resolve_legacy_default_input_path(input, "input", "analysis.json", "--input")
|
||||
plan_summary = None
|
||||
if plan:
|
||||
if not plan.exists():
|
||||
@@ -1165,9 +1226,9 @@ def report_duplicates(ctx: CLIContext, format: str, input: Path, output: Optiona
|
||||
@report.command('summary')
|
||||
@click.option(
|
||||
'--input',
|
||||
type=click.Path(exists=True, path_type=Path),
|
||||
default=Path('inventory.csv'),
|
||||
help='Input inventory CSV file (default: inventory.csv)'
|
||||
type=click.Path(path_type=Path),
|
||||
default=lambda: default_artifact_path('inventory.csv'),
|
||||
help='Input inventory CSV file (default: artifacts/inventory.csv)'
|
||||
)
|
||||
@click.option(
|
||||
'--output',
|
||||
@@ -1194,6 +1255,7 @@ def report_summary(ctx: CLIContext, input: Path, output: Optional[Path]):
|
||||
logger = ctx.logger
|
||||
|
||||
try:
|
||||
input = resolve_legacy_default_input_path(input, "input", "inventory.csv", "--input")
|
||||
# Load inventory from CSV
|
||||
click.echo(f"Loading inventory from: {input}")
|
||||
|
||||
@@ -1511,6 +1573,7 @@ def config_show(ctx: CLIContext):
|
||||
click.echo(f" Movie filename template: {cfg.movie_filename_template}")
|
||||
click.echo(f" Series filename template: {cfg.series_filename_template}")
|
||||
click.echo(f" Quarantine directory: {cfg.quarantine_dir}")
|
||||
click.echo(f" Workspace directory: {cfg.workspace_dir}")
|
||||
click.echo(f" Log level: {cfg.log_level}")
|
||||
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ class Config:
|
||||
series_filename_template: str = "S{season:02d}E{episode:02d}{ext}"
|
||||
log_level: str = "INFO"
|
||||
quarantine_dir: str = ".quarantine"
|
||||
workspace_dir: Path = Path("artifacts")
|
||||
categories: dict[str, list[str]] = field(default_factory=lambda: {
|
||||
"movie": ["movie", "movies"],
|
||||
"series": ["series", "tv", "shows"],
|
||||
@@ -85,6 +86,7 @@ def load_config(path: Path) -> Config:
|
||||
series_filename_template = templates.get("series_filename", "S{season:02d}E{episode:02d}{ext}")
|
||||
|
||||
quarantine_dir = data.get("quarantine_dir", ".quarantine")
|
||||
workspace_dir = Path(data.get("workspace_dir", "artifacts")).expanduser()
|
||||
log_level = data.get("log_level", "INFO")
|
||||
categories = data.get("categories", {
|
||||
"movie": ["movie", "movies"],
|
||||
@@ -116,6 +118,7 @@ def load_config(path: Path) -> Config:
|
||||
series_filename_template=series_filename_template,
|
||||
log_level=log_level,
|
||||
quarantine_dir=quarantine_dir,
|
||||
workspace_dir=workspace_dir,
|
||||
categories=categories,
|
||||
enrichment_enabled=enrichment.get("enabled", True),
|
||||
enrichment_incremental=enrichment.get("incremental", True),
|
||||
@@ -202,6 +205,7 @@ def create_default_config(path: Path) -> Config:
|
||||
"series_filename": default_config.series_filename_template,
|
||||
},
|
||||
"quarantine_dir": default_config.quarantine_dir,
|
||||
"workspace_dir": str(default_config.workspace_dir),
|
||||
"log_level": default_config.log_level,
|
||||
"categories": default_config.categories,
|
||||
"enrichment": enrichment_content,
|
||||
@@ -278,6 +282,11 @@ def validate_config(config: Config) -> list[str]:
|
||||
elif config.quarantine_dir.startswith("/") or config.quarantine_dir.startswith("\\"):
|
||||
errors.append("quarantine_dir must be relative to category root, not absolute")
|
||||
|
||||
if not isinstance(config.workspace_dir, Path):
|
||||
errors.append("workspace_dir must be a Path object")
|
||||
elif not str(config.workspace_dir).strip():
|
||||
errors.append("workspace_dir cannot be empty")
|
||||
|
||||
if not config.categories:
|
||||
errors.append("categories cannot be empty")
|
||||
elif not isinstance(config.categories, dict):
|
||||
|
||||
Reference in New Issue
Block a user