Initial upload: Code Privacy Shield v0.1.0
Some checks failed
CI / test (push) Has been cancelled

This commit is contained in:
2026-02-02 20:50:56 +00:00
parent 8bca22eb5c
commit fa7cff98de

View File

@@ -0,0 +1,225 @@
import sys
from pathlib import Path
from typing import List, Optional
import click
from . import __version__
from .config import Config
from .redactor import Redactor
from .patterns import PatternLibrary
@click.group()
@click.version_option(version=__version__)
@click.option("--config", "-c", type=click.Path(), help="Path to config file")
@click.option("--quiet", "-q", is_flag=True, help="Suppress output")
@click.pass_context
def main(ctx: click.Context, config: str, quiet: bool) -> None:
ctx.ensure_object(dict)
ctx.obj["config_path"] = config
ctx.obj["quiet"] = quiet if quiet is not None else False
@main.command()
@click.argument("paths", nargs=-1, type=click.Path(exists=True))
@click.option("--output", "-o", type=click.Path(), help="Output file (stdout if not specified)")
@click.option("--preview", "-p", is_flag=True, help="Preview what will be redacted")
@click.option("--in-place", "-i", is_flag=True, help="Edit files in place")
@click.option("--format", "output_format", type=click.Choice(["text", "json"]), default="text", help="Output format")
@click.pass_context
def redact(ctx: click.Context, paths: tuple, output: str, preview: bool, in_place: bool, output_format: str) -> None:
quiet = ctx.obj.get("quiet", False)
config_path = ctx.obj.get("config_path")
config = Config(config_path)
config.load()
if preview:
config.set("general.preview_mode", True)
pattern_library = PatternLibrary()
redactor = Redactor(pattern_library)
for custom_pattern in config.get_custom_patterns():
redactor.add_custom_pattern(
name=custom_pattern.get("name", "Custom"),
pattern=custom_pattern.get("pattern", ""),
category=custom_pattern.get("category", "custom"),
)
if not paths:
content = click.get_text_stream("stdin").read()
result = redactor.redact(content, preserve_structure=config.should_preserve_structure())
if preview:
_show_preview(result, output_format)
else:
click.echo(result.redacted)
return
for path_str in paths:
path = Path(path_str)
if path.is_file():
_process_file(redactor, path, output, preview, in_place, output_format, config)
elif path.is_dir():
_process_directory(redactor, path, output, preview, in_place, output_format, config)
def _process_file(
redactor: Redactor,
path: Path,
output: Optional[str],
preview: bool,
in_place: bool,
output_format: str,
config: Config
) -> None:
try:
result = redactor.redact_file(str(path), encoding="utf-8")
if preview:
if not config.is_quiet_mode():
click.echo(f"\n=== {path} ===")
_show_preview(result, output_format)
elif in_place:
path.write_text(result.redacted, encoding="utf-8")
if not config.is_quiet_mode():
summary = redactor.get_match_summary(result)
click.echo(f"Redacted {path}: {len(result.matches)} matches found")
elif output:
Path(output).write_text(result.redacted, encoding="utf-8")
else:
click.echo(result.redacted)
except (IOError, UnicodeDecodeError) as e:
click.echo(f"Error processing {path}: {e}", err=True)
def _process_directory(
redactor: Redactor,
path: Path,
output: Optional[str],
preview: bool,
in_place: bool,
output_format: str,
config: Config
) -> None:
exclude_patterns = config.get_exclude_patterns()
for file_path in path.rglob("*"):
if file_path.is_file():
if _should_exclude(file_path, exclude_patterns):
continue
_process_file(redactor, file_path, output, preview, in_place, output_format, config)
def _should_exclude(file_path: Path, exclude_patterns: List[str]) -> bool:
for pattern in exclude_patterns:
if file_path.match(pattern):
return True
if pattern.startswith("*") and file_path.name.endswith(pattern[1:]):
return True
return False
def _show_preview(result, output_format: str) -> None:
if output_format == "json":
import json
preview_data = {
"matches": [
{
"category": m.category,
"name": m.name,
"original": m.original,
"replacement": m.replacement,
}
for m in result.matches
],
"total_matches": len(result.matches),
"categories": list(result.categories),
}
click.echo(json.dumps(preview_data, indent=2))
else:
for match in result.matches:
click.echo(f"[{match.category}] {match.name}: {match.original} -> {match.replacement}")
@main.command()
@click.argument("path", type=click.Path())
@click.option("--format", "output_format", type=click.Choice(["text", "json"]), default="text", help="Output format")
@click.pass_context
def preview(ctx: click.Context, path: str, output_format: str) -> None:
ctx.invoke(redact, paths=(path,), preview=True, output_format=output_format)
@main.command()
@click.argument("path", type=click.Path())
@click.pass_context
def check(ctx: click.Context, path: str) -> None:
config_path = ctx.obj.get("config_path")
config = Config(config_path)
config.load()
pattern_library = PatternLibrary()
redactor = Redactor(pattern_library)
try:
if Path(path).is_file():
result = redactor.redact_file(path, encoding="utf-8")
else:
content = Path(path).read_text(encoding="utf-8")
result = redactor.redact(content)
if result.matches:
click.echo(f"Found {len(result.matches)} potential sensitive items:")
summary = redactor.get_match_summary(result)
for key, count in sorted(summary.items()):
click.echo(f" - {key}: {count}")
sys.exit(1)
else:
click.echo("No sensitive data detected.")
sys.exit(0)
except (IOError, UnicodeDecodeError) as e:
click.echo(f"Error reading {path}: {e}", err=True)
sys.exit(1)
@main.command()
@click.argument("path", type=click.Path())
@click.pass_context
def init_config(ctx: click.Context, path: str) -> None:
if Config.create_example_config(path):
click.echo(f"Created example config at {path}")
else:
click.echo("Failed to create config file", err=True)
sys.exit(1)
@main.command()
@click.pass_context
def config_locations(ctx: click.Context) -> None:
click.echo(f"Project config: {Config.get_project_config_path()}")
click.echo(f"User config: {Config.get_default_config_path()}")
@main.group()
def config() -> None:
"""Manage configuration."""
pass
@config.command("show")
@click.pass_context
def config_show(ctx: click.Context) -> None:
import toml
config_path = ctx.obj.get("config_path")
config = Config(config_path)
config.load()
click.echo(toml.dumps(config.config))