Picarones / picarones /interfaces /cli /import_corpus.py
Claude
fix(architecture): audit complet — corrige les violations de couches détectées par CI
162c559 unverified
Raw
History Blame
4.83 kB
"""``picarones-rewrite import-corpus`` — extraction sandboxée d'un ZIP.
Sprint A14-S22.
Wrapper CLI minimal autour du ``CorpusService`` (S20) :
::
python -m picarones.interfaces.cli import-corpus mon_corpus.zip \\
--output-dir ./workspaces/sess1 \\
--corpus-name bnf_xviiie \\
--metadata language=fr \\
--metadata period=early_modern
Comportement
------------
- Lit le ZIP (path utilisateur, sans validation préalable — la CLI
fait confiance au filesystem local de l'opérateur).
- Crée un ``WorkspaceManager`` dans ``--output-dir`` (créé s'il
n'existe pas).
- Appelle ``CorpusService.import_zip``.
- Affiche un résumé lisible : n_documents, n_images sans GT, GT
orphelines, warnings.
- Code de sortie ``0`` succès, ``1`` erreur typée
(``CorpusImportError``), ``2`` erreur d'usage Click (gérée par
Click).
"""
from __future__ import annotations
import sys
from pathlib import Path
import click
from picarones.app.services import (
CorpusImportError,
CorpusService,
WorkspaceManager,
)
@click.command()
@click.argument(
"zip_path",
type=click.Path(
exists=True, dir_okay=False, file_okay=True, path_type=Path,
),
)
@click.option(
"--output-dir",
"output_dir",
type=click.Path(file_okay=False, dir_okay=True, path_type=Path),
required=True,
help=(
"Répertoire parent où créer le workspace sandboxé. Créé "
"s'il n'existe pas."
),
)
@click.option(
"--corpus-name",
default=None,
help=(
"Nom du corpus (défaut : nom du fichier ZIP sans "
"extension). Sera sanitizé automatiquement."
),
)
@click.option(
"--metadata",
"metadata_pairs",
multiple=True,
help=(
"Paires ``clé=valeur`` (option répétable). Ex : "
"``--metadata language=fr --metadata period=medieval``."
),
)
@click.option(
"--max-zip-mb",
default=100,
type=int,
show_default=True,
help="Plafond taille du blob ZIP (Mo).",
)
@click.option(
"--max-entries",
default=5000,
type=int,
show_default=True,
help="Plafond nombre d'entrées dans le ZIP (anti zip bomb).",
)
@click.option(
"--max-uncompressed-mb",
default=500,
type=int,
show_default=True,
help="Plafond taille décompressée totale (Mo).",
)
@click.option(
"--quiet",
is_flag=True,
default=False,
help="N'affiche que le chemin du dossier extrait, rien d'autre.",
)
def import_corpus_command(
zip_path: Path,
output_dir: Path,
corpus_name: str | None,
metadata_pairs: tuple[str, ...],
max_zip_mb: int,
max_entries: int,
max_uncompressed_mb: int,
quiet: bool,
) -> None:
"""Extrait un ZIP de corpus dans un workspace sandboxé."""
output_dir.mkdir(parents=True, exist_ok=True)
workspace = WorkspaceManager(output_dir)
if corpus_name is None:
corpus_name = zip_path.stem
metadata = _parse_metadata_pairs(metadata_pairs)
service = CorpusService(
workspace,
max_zip_size_bytes=max_zip_mb * 1024 * 1024,
max_entry_count=max_entries,
max_uncompressed_bytes=max_uncompressed_mb * 1024 * 1024,
)
try:
report = service.import_zip(
zip_path.read_bytes(),
corpus_name=corpus_name,
metadata=metadata,
)
except CorpusImportError as exc:
click.echo(f"erreur : {exc}", err=True)
sys.exit(1)
if quiet:
click.echo(str(report.extracted_dir))
return
click.echo(f"Corpus extrait dans : {report.extracted_dir}")
click.echo(f" documents : {report.n_documents}")
click.echo(f" sans GT : {report.n_images_without_gt}")
click.echo(f" GT orphelines : {report.n_gt_without_image}")
click.echo(f" bruit OS sauté : {report.n_skipped_noise}")
if report.warnings:
click.echo("Avertissements :")
for w in report.warnings:
click.echo(f" - {w}")
def _parse_metadata_pairs(
pairs: tuple[str, ...],
) -> dict[str, str]:
"""Parse ``("k1=v1", "k2=v2")`` → ``{"k1": "v1", "k2": "v2"}``.
Lève ``click.BadParameter`` si une paire ne contient pas ``=``.
"""
out: dict[str, str] = {}
for pair in pairs:
if "=" not in pair:
raise click.BadParameter(
f"métadonnée invalide : {pair!r} (attendu ``clé=valeur``).",
param_hint="--metadata",
)
key, _, value = pair.partition("=")
key = key.strip()
value = value.strip()
if not key:
raise click.BadParameter(
f"métadonnée à clé vide : {pair!r}.",
param_hint="--metadata",
)
out[key] = value
return out
__all__ = ["import_corpus_command"]