feat(api): expose apply_zip() in the public API

Adds apply_zip() alongside get_files/get_git_changes/create_zip,
following the same plain-function, exception-raising style (no Click,
no Rich, no SystemExit). Exposes ApplyResult and ZipNotFoundError
from the package root.
This commit is contained in:
akadeepesh
2026-08-24 08:59:44 +05:30
parent 00dba7ac8c
commit ca14e0f6a0
2 changed files with 87 additions and 0 deletions
+6
View File
@@ -9,26 +9,32 @@ from contextzip.api import (
GitNotFoundError,
GitCommandError,
NoFilesError,
ZipNotFoundError,
get_git_changes,
get_files,
create_zip,
apply_zip,
detect_ecosystem,
)
from contextzip.packager import PackageResult
from contextzip.applier import ApplyResult
__all__ = [
# Functions
"get_git_changes",
"get_files",
"create_zip",
"apply_zip",
"detect_ecosystem",
# Data types
"FileCollection",
"PackageResult",
"ApplyResult",
# Exceptions
"ContextzipError",
"NotARepositoryError",
"GitNotFoundError",
"GitCommandError",
"NoFilesError",
"ZipNotFoundError",
]
+81
View File
@@ -39,6 +39,7 @@ import os
from dataclasses import dataclass, field
from pathlib import Path
from contextzip.applier import ApplyResult
from contextzip.detector import DetectionResult, detect
from contextzip.filters import (
ResolveResult,
@@ -127,6 +128,10 @@ class NoFilesError(ContextzipError):
"""Raised when file resolution produces an empty result."""
class ZipNotFoundError(ContextzipError):
"""Raised when apply_zip() can't resolve which zip to apply."""
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
@@ -335,6 +340,82 @@ def create_zip(
)
def apply_zip(
zip_path: str | Path | None = None,
*,
project_dir: str | Path | None = None,
manifest: str | Path | None = None,
) -> ApplyResult:
"""
Apply an AI-returned ZIP back into *project_dir* (defaults to cwd).
Diffs the zip against the local sidecar manifest most recently written
to ``.contextzip/output/`` (or the one at *manifest*, if given) to
classify every file as new, modified, unchanged, or one with no safe
baseline (locally edited since zipping, or never part of the original
manifest). Only adds and modifies files — nothing is ever deleted.
Every overwritten file is backed up first, under
``.contextzip/backups/<timestamp>/``.
Unlike the CLI, this applies immediately and does not prompt — check
``contextzip.applier.build_plan()`` yourself first if you want to
inspect risk before writing (see ``ApplyPlan.is_risky`` /
``.risky_entries``).
Parameters
----------
zip_path:
Path to the returned zip. If omitted, auto-detects from
``.contextzip/inbox/`` — exactly one zip must be present there.
project_dir:
Project root to apply into. Defaults to ``Path.cwd()``.
manifest:
Explicit manifest path to diff against, overriding auto-detection.
Returns
-------
ApplyResult
``written`` (list of relative paths written), ``backup_dir``
(``Path`` or ``None`` if nothing needed backing up), and
``applied_zip_path`` (where the consumed zip ended up).
Raises
------
ZipNotFoundError
If no zip could be resolved — missing, or multiple candidates in
the inbox with none specified.
Example
-------
::
from contextzip import apply_zip
result = apply_zip() # picks up .contextzip/inbox/*.zip
print(f"Wrote {len(result.written)} files")
if result.backup_dir:
print(f"Backup at {result.backup_dir}")
"""
from contextzip.applier import (
ApplyError,
build_plan,
execute_plan,
find_latest_manifest,
find_zip_to_apply,
)
pdir = _resolve_dir(project_dir)
try:
resolved_zip = find_zip_to_apply(pdir, zip_path)
except ApplyError as exc:
raise ZipNotFoundError(str(exc)) from exc
manifest_path = find_latest_manifest(pdir, manifest)
plan = build_plan(resolved_zip, pdir, manifest_path)
return execute_plan(plan, pdir)
def detect_ecosystem(
path: str | Path | None = None,
) -> DetectionResult: