contextzip 0.3.0__tar.gz → 0.3.2__tar.gz
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.
- {contextzip-0.3.0 → contextzip-0.3.2}/PKG-INFO +38 -2
- {contextzip-0.3.0 → contextzip-0.3.2}/README.md +37 -1
- contextzip-0.3.2/contextzip/__init__.py +34 -0
- contextzip-0.3.2/contextzip/api.py +400 -0
- contextzip-0.3.2/contextzip/cli.py +680 -0
- contextzip-0.3.2/contextzip/cli_ai.py +160 -0
- contextzip-0.3.2/contextzip/cli_display.py +331 -0
- contextzip-0.3.2/contextzip/cli_onboard.py +124 -0
- {contextzip-0.3.0 → contextzip-0.3.2}/contextzip/clipboard.py +15 -10
- {contextzip-0.3.0 → contextzip-0.3.2}/contextzip/config.py +3 -0
- {contextzip-0.3.0 → contextzip-0.3.2}/contextzip/detector.py +12 -13
- contextzip-0.3.2/contextzip/error_parser.py +506 -0
- {contextzip-0.3.0 → contextzip-0.3.2}/contextzip/filters.py +24 -20
- {contextzip-0.3.0 → contextzip-0.3.2}/contextzip/git.py +21 -14
- {contextzip-0.3.0 → contextzip-0.3.2}/contextzip/packager.py +111 -19
- {contextzip-0.3.0 → contextzip-0.3.2}/contextzip/rules/base.py +1 -9
- contextzip-0.3.2/contextzip/rules/errors/__init__.py +0 -0
- contextzip-0.3.2/contextzip/rules/errors/node.py +506 -0
- contextzip-0.3.2/contextzip/rules/errors/python.py +103 -0
- {contextzip-0.3.0 → contextzip-0.3.2}/contextzip/rules/node.py +1 -1
- {contextzip-0.3.0 → contextzip-0.3.2}/contextzip/rules/python.py +4 -4
- {contextzip-0.3.0 → contextzip-0.3.2}/contextzip/rules/ruby.py +6 -13
- {contextzip-0.3.0 → contextzip-0.3.2}/contextzip/rules/rust.py +1 -1
- contextzip-0.3.2/contextzip/watcher.py +662 -0
- {contextzip-0.3.0 → contextzip-0.3.2}/contextzip.egg-info/PKG-INFO +38 -2
- {contextzip-0.3.0 → contextzip-0.3.2}/contextzip.egg-info/SOURCES.txt +10 -1
- {contextzip-0.3.0 → contextzip-0.3.2}/pyproject.toml +2 -2
- contextzip-0.3.0/contextzip/__init__.py +0 -3
- contextzip-0.3.0/contextzip/cli.py +0 -1044
- {contextzip-0.3.0 → contextzip-0.3.2}/LICENSE +0 -0
- {contextzip-0.3.0 → contextzip-0.3.2}/contextzip/rules/__init__.py +0 -0
- {contextzip-0.3.0 → contextzip-0.3.2}/contextzip/rules/go.py +0 -0
- {contextzip-0.3.0 → contextzip-0.3.2}/contextzip.egg-info/dependency_links.txt +0 -0
- {contextzip-0.3.0 → contextzip-0.3.2}/contextzip.egg-info/entry_points.txt +0 -0
- {contextzip-0.3.0 → contextzip-0.3.2}/contextzip.egg-info/requires.txt +0 -0
- {contextzip-0.3.0 → contextzip-0.3.2}/contextzip.egg-info/top_level.txt +0 -0
- {contextzip-0.3.0 → contextzip-0.3.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: contextzip
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Intelligently package your codebase for AI tools
|
|
5
5
|
Author-email: Deepesh <akadeepesh@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -51,6 +51,7 @@ contextzip eliminates that entirely. Run it from your project root — it detect
|
|
|
51
51
|
- **Respects `.gitignore`** — your existing ignore patterns are honoured automatically
|
|
52
52
|
- **Git-aware packaging** — use `--git-changes` to package only modified, staged, and untracked files; perfect for incremental debugging and PR review sessions
|
|
53
53
|
- **AI-powered file selection** — describe your task in plain English with `--prompt` and Gemini selects the minimum relevant files automatically, no manual hunting required
|
|
54
|
+
- **Terminal error watcher** — wrap any dev server with `contextzip watch` to auto-detect errors and package a ready-to-upload debug context in one keypress
|
|
54
55
|
- **Persistent workspace** — all generated ZIPs land in `.contextzip/` at your project root, discoverable, reusable, and git-ignored automatically
|
|
55
56
|
- **Warns before it's a problem** — flags large (≥ 1 MB) and binary files that AI tools can't read, before you waste an upload
|
|
56
57
|
- **Handles edge cases** — dangling symlinks, unreadable files, and paths outside the project tree are caught and reported, never silently dropped
|
|
@@ -116,7 +117,7 @@ contextzip [OPTIONS]
|
|
|
116
117
|
| `--no-clipboard` | Skip the clipboard / folder-open step |
|
|
117
118
|
| `--no-gitignore` | Ignore the project's `.gitignore` |
|
|
118
119
|
|
|
119
|
-
**Subcommands:** `exclude`, `include`, `config` — run `contextzip --help` for full details.
|
|
120
|
+
**Subcommands:** `exclude`, `include`, `watch`, `config` — run `contextzip --help` for full details.
|
|
120
121
|
|
|
121
122
|
---
|
|
122
123
|
|
|
@@ -173,6 +174,41 @@ contextzip config --reset-key # clear and re-run setup
|
|
|
173
174
|
|
|
174
175
|
---
|
|
175
176
|
|
|
177
|
+
## Terminal error watcher
|
|
178
|
+
|
|
179
|
+
The `watch` command wraps your dev server, buffers its output, and packages a debug-ready ZIP the moment you spot an error — no manual file hunting, no copy-pasting stack traces.
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
contextzip watch -- npm run dev
|
|
183
|
+
contextzip watch -- python manage.py runserver
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
contextzip starts your process normally. You see output exactly as you would without it. In the background, it watches the stream for errors. When one is detected, a prompt appears directly beneath the error output:
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
╭─ contextzip · error detected ─────────────────────╮
|
|
190
|
+
│ Press [D] to package debug context [S] to skip │
|
|
191
|
+
╰───────────────────────────────────────────────────╯
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Press **D** and contextzip immediately writes `.contextzip/debug-context.zip`. Your server keeps running — no restart, no interruption.
|
|
195
|
+
|
|
196
|
+
**What's in the ZIP:**
|
|
197
|
+
|
|
198
|
+
| File | Contents |
|
|
199
|
+
|---|---|
|
|
200
|
+
| `prompt.txt` | Auto-generated: detected framework, error type, and task description — ready to paste into any AI tool |
|
|
201
|
+
| `terminal-error.txt` | The cleaned, noise-stripped error block and stack trace |
|
|
202
|
+
| `source-files.zip` | Source files referenced in the stack trace, paths preserved |
|
|
203
|
+
|
|
204
|
+
**On Ctrl+C:** If no errors were packaged during the session, contextzip offers one final prompt to capture the full session output — useful when something looked wrong but didn't match a known error pattern.
|
|
205
|
+
|
|
206
|
+
**Supported frameworks:** Python, Django, FastAPI, Node.js, Next.js, React. Each has its own error detection patterns and noise filters so the output stays clean across stacks.
|
|
207
|
+
|
|
208
|
+
> **Note:** `watch` works best with dev servers that don't read stdin interactively (`npm run dev`, `manage.py runserver`, etc.). PTY emulation is not used — on Windows, color passthrough may be limited.
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
176
212
|
## What gets excluded
|
|
177
213
|
|
|
178
214
|
contextzip stacks exclusion rules based on your detected stack, on top of your `.gitignore`.
|
|
@@ -22,6 +22,7 @@ contextzip eliminates that entirely. Run it from your project root — it detect
|
|
|
22
22
|
- **Respects `.gitignore`** — your existing ignore patterns are honoured automatically
|
|
23
23
|
- **Git-aware packaging** — use `--git-changes` to package only modified, staged, and untracked files; perfect for incremental debugging and PR review sessions
|
|
24
24
|
- **AI-powered file selection** — describe your task in plain English with `--prompt` and Gemini selects the minimum relevant files automatically, no manual hunting required
|
|
25
|
+
- **Terminal error watcher** — wrap any dev server with `contextzip watch` to auto-detect errors and package a ready-to-upload debug context in one keypress
|
|
25
26
|
- **Persistent workspace** — all generated ZIPs land in `.contextzip/` at your project root, discoverable, reusable, and git-ignored automatically
|
|
26
27
|
- **Warns before it's a problem** — flags large (≥ 1 MB) and binary files that AI tools can't read, before you waste an upload
|
|
27
28
|
- **Handles edge cases** — dangling symlinks, unreadable files, and paths outside the project tree are caught and reported, never silently dropped
|
|
@@ -87,7 +88,7 @@ contextzip [OPTIONS]
|
|
|
87
88
|
| `--no-clipboard` | Skip the clipboard / folder-open step |
|
|
88
89
|
| `--no-gitignore` | Ignore the project's `.gitignore` |
|
|
89
90
|
|
|
90
|
-
**Subcommands:** `exclude`, `include`, `config` — run `contextzip --help` for full details.
|
|
91
|
+
**Subcommands:** `exclude`, `include`, `watch`, `config` — run `contextzip --help` for full details.
|
|
91
92
|
|
|
92
93
|
---
|
|
93
94
|
|
|
@@ -144,6 +145,41 @@ contextzip config --reset-key # clear and re-run setup
|
|
|
144
145
|
|
|
145
146
|
---
|
|
146
147
|
|
|
148
|
+
## Terminal error watcher
|
|
149
|
+
|
|
150
|
+
The `watch` command wraps your dev server, buffers its output, and packages a debug-ready ZIP the moment you spot an error — no manual file hunting, no copy-pasting stack traces.
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
contextzip watch -- npm run dev
|
|
154
|
+
contextzip watch -- python manage.py runserver
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
contextzip starts your process normally. You see output exactly as you would without it. In the background, it watches the stream for errors. When one is detected, a prompt appears directly beneath the error output:
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
╭─ contextzip · error detected ─────────────────────╮
|
|
161
|
+
│ Press [D] to package debug context [S] to skip │
|
|
162
|
+
╰───────────────────────────────────────────────────╯
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Press **D** and contextzip immediately writes `.contextzip/debug-context.zip`. Your server keeps running — no restart, no interruption.
|
|
166
|
+
|
|
167
|
+
**What's in the ZIP:**
|
|
168
|
+
|
|
169
|
+
| File | Contents |
|
|
170
|
+
|---|---|
|
|
171
|
+
| `prompt.txt` | Auto-generated: detected framework, error type, and task description — ready to paste into any AI tool |
|
|
172
|
+
| `terminal-error.txt` | The cleaned, noise-stripped error block and stack trace |
|
|
173
|
+
| `source-files.zip` | Source files referenced in the stack trace, paths preserved |
|
|
174
|
+
|
|
175
|
+
**On Ctrl+C:** If no errors were packaged during the session, contextzip offers one final prompt to capture the full session output — useful when something looked wrong but didn't match a known error pattern.
|
|
176
|
+
|
|
177
|
+
**Supported frameworks:** Python, Django, FastAPI, Node.js, Next.js, React. Each has its own error detection patterns and noise filters so the output stays clean across stacks.
|
|
178
|
+
|
|
179
|
+
> **Note:** `watch` works best with dev servers that don't read stdin interactively (`npm run dev`, `manage.py runserver`, etc.). PTY emulation is not used — on Windows, color passthrough may be limited.
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
147
183
|
## What gets excluded
|
|
148
184
|
|
|
149
185
|
contextzip stacks exclusion rules based on your detected stack, on top of your `.gitignore`.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""contextzip — intelligent codebase packager for AI tools."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.3.2"
|
|
4
|
+
|
|
5
|
+
from contextzip.api import (
|
|
6
|
+
FileCollection,
|
|
7
|
+
ContextzipError,
|
|
8
|
+
NotARepositoryError,
|
|
9
|
+
GitNotFoundError,
|
|
10
|
+
GitCommandError,
|
|
11
|
+
NoFilesError,
|
|
12
|
+
get_git_changes,
|
|
13
|
+
get_files,
|
|
14
|
+
create_zip,
|
|
15
|
+
detect_ecosystem,
|
|
16
|
+
)
|
|
17
|
+
from contextzip.packager import PackageResult
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
# Functions
|
|
21
|
+
"get_git_changes",
|
|
22
|
+
"get_files",
|
|
23
|
+
"create_zip",
|
|
24
|
+
"detect_ecosystem",
|
|
25
|
+
# Data types
|
|
26
|
+
"FileCollection",
|
|
27
|
+
"PackageResult",
|
|
28
|
+
# Exceptions
|
|
29
|
+
"ContextzipError",
|
|
30
|
+
"NotARepositoryError",
|
|
31
|
+
"GitNotFoundError",
|
|
32
|
+
"GitCommandError",
|
|
33
|
+
"NoFilesError",
|
|
34
|
+
]
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
"""
|
|
2
|
+
api.py — Public Python API for contextzip.
|
|
3
|
+
|
|
4
|
+
Exposes the same capabilities as the CLI but as plain Python functions:
|
|
5
|
+
no Click, no Rich output, no SystemExit. All functions raise exceptions
|
|
6
|
+
on failure so callers can handle errors in their own way.
|
|
7
|
+
|
|
8
|
+
Quickstart
|
|
9
|
+
──────────
|
|
10
|
+
from contextzip import get_git_changes, get_files, create_zip
|
|
11
|
+
|
|
12
|
+
# Get git-changed files and use them directly (no zip needed)
|
|
13
|
+
collection = get_git_changes()
|
|
14
|
+
for path in collection.files:
|
|
15
|
+
upload(path) # plain pathlib.Path objects
|
|
16
|
+
|
|
17
|
+
# Or zip them
|
|
18
|
+
pkg = create_zip(collection, output="/tmp/changes.zip")
|
|
19
|
+
with open(pkg.zip_path, "rb") as f:
|
|
20
|
+
upload_to_s3(f)
|
|
21
|
+
|
|
22
|
+
# Get all project files (respecting .gitignore and built-in rules)
|
|
23
|
+
collection = get_files()
|
|
24
|
+
for path in collection.files:
|
|
25
|
+
print(path)
|
|
26
|
+
|
|
27
|
+
# Narrow it down
|
|
28
|
+
collection = get_files(
|
|
29
|
+
include=["src/", "app/"],
|
|
30
|
+
exclude=["tests/", "*.log"],
|
|
31
|
+
)
|
|
32
|
+
pkg = create_zip(collection, output="/tmp/upload.zip")
|
|
33
|
+
print(f"Packed {pkg.file_count} files → {pkg.zip_path}")
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import os
|
|
39
|
+
from dataclasses import dataclass, field
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
|
|
42
|
+
from contextzip.detector import DetectionResult, detect
|
|
43
|
+
from contextzip.filters import (
|
|
44
|
+
ResolveResult,
|
|
45
|
+
build_spec,
|
|
46
|
+
resolve_files,
|
|
47
|
+
resolve_files_from_git,
|
|
48
|
+
)
|
|
49
|
+
from contextzip.git import GitChanges, GitError, GitErrorKind, get_changed_files
|
|
50
|
+
from contextzip.packager import PackageResult, create_zip_silent
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
# Public result type
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class FileCollection:
|
|
60
|
+
"""
|
|
61
|
+
A resolved set of files returned by :func:`get_git_changes` or
|
|
62
|
+
:func:`get_files`.
|
|
63
|
+
|
|
64
|
+
``files`` is always a plain list of absolute :class:`pathlib.Path` objects
|
|
65
|
+
— use them directly for uploads, processing, or anything else.
|
|
66
|
+
Zipping is optional: pass this object to :func:`create_zip` if needed.
|
|
67
|
+
|
|
68
|
+
Attributes
|
|
69
|
+
----------
|
|
70
|
+
files:
|
|
71
|
+
Absolute paths of all files in this collection. These are the
|
|
72
|
+
files that would be (or were) included in a ZIP.
|
|
73
|
+
skipped:
|
|
74
|
+
Files that were silently skipped during resolution, as
|
|
75
|
+
``(path, reason)`` tuples (e.g. dangling symlinks, unreadable files).
|
|
76
|
+
large_files:
|
|
77
|
+
Files exceeding 1 MB, as ``(path, size_in_bytes)`` tuples.
|
|
78
|
+
They are still included in ``files`` — this is informational only.
|
|
79
|
+
binary_files:
|
|
80
|
+
Files that appear to be binary (contain null bytes). Still included
|
|
81
|
+
in ``files`` — informational only.
|
|
82
|
+
project_dir:
|
|
83
|
+
The project root used when resolving paths.
|
|
84
|
+
ecosystem:
|
|
85
|
+
Detected ecosystem string, e.g. ``"Next.js + Node.js"``.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
files: list[Path] = field(default_factory=list)
|
|
89
|
+
skipped: list[tuple[Path, str]] = field(default_factory=list)
|
|
90
|
+
large_files: list[tuple[Path, int]] = field(default_factory=list)
|
|
91
|
+
binary_files: list[Path] = field(default_factory=list)
|
|
92
|
+
project_dir: Path = field(default_factory=Path.cwd)
|
|
93
|
+
ecosystem: str = "Unknown"
|
|
94
|
+
|
|
95
|
+
def __len__(self) -> int:
|
|
96
|
+
return len(self.files)
|
|
97
|
+
|
|
98
|
+
def __iter__(self):
|
|
99
|
+
return iter(self.files)
|
|
100
|
+
|
|
101
|
+
def __bool__(self) -> bool:
|
|
102
|
+
return bool(self.files)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
# Exceptions
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class ContextzipError(Exception):
|
|
111
|
+
"""Base class for all contextzip API errors."""
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class NotARepositoryError(ContextzipError):
|
|
115
|
+
"""Raised when the project directory is not inside a git repository."""
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class GitNotFoundError(ContextzipError):
|
|
119
|
+
"""Raised when git is not installed or not on PATH."""
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class GitCommandError(ContextzipError):
|
|
123
|
+
"""Raised when a git command fails unexpectedly."""
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class NoFilesError(ContextzipError):
|
|
127
|
+
"""Raised when file resolution produces an empty result."""
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# ---------------------------------------------------------------------------
|
|
131
|
+
# Public API
|
|
132
|
+
# ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def get_git_changes(
|
|
136
|
+
path: str | Path | None = None,
|
|
137
|
+
) -> FileCollection:
|
|
138
|
+
"""
|
|
139
|
+
Return the files that git reports as modified, added, or untracked.
|
|
140
|
+
|
|
141
|
+
The git root is found automatically by walking up from *path* (or the
|
|
142
|
+
current working directory). Files are filtered through contextzip's base
|
|
143
|
+
safety rules so secrets, binaries, and similar files are always excluded.
|
|
144
|
+
|
|
145
|
+
Parameters
|
|
146
|
+
----------
|
|
147
|
+
path:
|
|
148
|
+
Directory to start from. Defaults to ``Path.cwd()``. The actual
|
|
149
|
+
git root may be a parent of this directory.
|
|
150
|
+
|
|
151
|
+
Returns
|
|
152
|
+
-------
|
|
153
|
+
FileCollection
|
|
154
|
+
``collection.files`` contains absolute :class:`~pathlib.Path` objects
|
|
155
|
+
for every changed file. Iterate over it or pass it to
|
|
156
|
+
:func:`create_zip`.
|
|
157
|
+
|
|
158
|
+
Raises
|
|
159
|
+
------
|
|
160
|
+
GitNotFoundError
|
|
161
|
+
If git is not installed or not on PATH.
|
|
162
|
+
NotARepositoryError
|
|
163
|
+
If *path* is not inside a git repository.
|
|
164
|
+
GitCommandError
|
|
165
|
+
If ``git status`` fails for any other reason.
|
|
166
|
+
|
|
167
|
+
Example
|
|
168
|
+
-------
|
|
169
|
+
::
|
|
170
|
+
|
|
171
|
+
from contextzip import get_git_changes
|
|
172
|
+
|
|
173
|
+
collection = get_git_changes()
|
|
174
|
+
for f in collection.files:
|
|
175
|
+
print(f) # plain pathlib.Path — use however you like
|
|
176
|
+
|
|
177
|
+
# Only staged files
|
|
178
|
+
for rel in collection._git_changes.staged:
|
|
179
|
+
print(rel)
|
|
180
|
+
"""
|
|
181
|
+
project_dir = _resolve_dir(path)
|
|
182
|
+
git_result = get_changed_files(project_dir)
|
|
183
|
+
|
|
184
|
+
if isinstance(git_result, GitError):
|
|
185
|
+
_raise_git_error(git_result)
|
|
186
|
+
|
|
187
|
+
if git_result.is_empty:
|
|
188
|
+
return FileCollection(project_dir=project_dir)
|
|
189
|
+
|
|
190
|
+
resolved = resolve_files_from_git(
|
|
191
|
+
git_files=git_result.files,
|
|
192
|
+
project_dir=project_dir,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
detection = detect(project_dir)
|
|
196
|
+
|
|
197
|
+
collection = _resolve_result_to_collection(resolved, project_dir, detection)
|
|
198
|
+
# Stash the raw GitChanges on the collection for callers who want
|
|
199
|
+
# staged/unstaged/untracked breakdowns without accessing internals
|
|
200
|
+
collection._git_changes = git_result # type: ignore[attr-defined]
|
|
201
|
+
return collection
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def get_files(
|
|
205
|
+
path: str | Path | None = None,
|
|
206
|
+
*,
|
|
207
|
+
include: list[str] | None = None,
|
|
208
|
+
exclude: list[str] | None = None,
|
|
209
|
+
use_gitignore: bool = True,
|
|
210
|
+
) -> FileCollection:
|
|
211
|
+
"""
|
|
212
|
+
Return all project files after applying contextzip's standard exclusion rules.
|
|
213
|
+
|
|
214
|
+
Parameters
|
|
215
|
+
----------
|
|
216
|
+
path:
|
|
217
|
+
Project root to scan. Defaults to ``Path.cwd()``.
|
|
218
|
+
include:
|
|
219
|
+
If given, only files under these paths are returned (e.g.
|
|
220
|
+
``["src/", "app/"]``). Matched as exact path prefixes.
|
|
221
|
+
exclude:
|
|
222
|
+
Extra exclusion patterns on top of the auto-detected rules
|
|
223
|
+
(gitignore syntax, e.g. ``["tests/", "*.log"]``).
|
|
224
|
+
use_gitignore:
|
|
225
|
+
Whether to apply the project's ``.gitignore`` file.
|
|
226
|
+
Defaults to ``True``.
|
|
227
|
+
|
|
228
|
+
Returns
|
|
229
|
+
-------
|
|
230
|
+
FileCollection
|
|
231
|
+
``collection.files`` contains absolute :class:`~pathlib.Path` objects
|
|
232
|
+
for every included file.
|
|
233
|
+
|
|
234
|
+
Example
|
|
235
|
+
-------
|
|
236
|
+
::
|
|
237
|
+
|
|
238
|
+
from contextzip import get_files
|
|
239
|
+
|
|
240
|
+
# All project files
|
|
241
|
+
collection = get_files()
|
|
242
|
+
|
|
243
|
+
# Only src/, excluding tests
|
|
244
|
+
collection = get_files(include=["src/"], exclude=["tests/"])
|
|
245
|
+
|
|
246
|
+
for f in collection.files:
|
|
247
|
+
process(f)
|
|
248
|
+
"""
|
|
249
|
+
project_dir = _resolve_dir(path)
|
|
250
|
+
detection = detect(project_dir)
|
|
251
|
+
|
|
252
|
+
gitignore_path = (project_dir / ".gitignore") if use_gitignore else None
|
|
253
|
+
|
|
254
|
+
spec = build_spec(
|
|
255
|
+
rule_modules=detection.rule_modules,
|
|
256
|
+
extra_exclude=exclude or None,
|
|
257
|
+
gitignore_path=gitignore_path,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
resolved = resolve_files(
|
|
261
|
+
project_dir=project_dir,
|
|
262
|
+
spec=spec,
|
|
263
|
+
include_only=include or None,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
return _resolve_result_to_collection(resolved, project_dir, detection)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def create_zip(
|
|
270
|
+
collection: FileCollection,
|
|
271
|
+
output: str | Path | None = None,
|
|
272
|
+
) -> PackageResult:
|
|
273
|
+
"""
|
|
274
|
+
Write *collection* into a ZIP archive and return the result.
|
|
275
|
+
|
|
276
|
+
Parameters
|
|
277
|
+
----------
|
|
278
|
+
collection:
|
|
279
|
+
A :class:`FileCollection` returned by :func:`get_git_changes` or
|
|
280
|
+
:func:`get_files`.
|
|
281
|
+
output:
|
|
282
|
+
Where to write the ZIP. If omitted, the archive is written to the
|
|
283
|
+
``.contextzip/`` workspace at the project root (same as the CLI).
|
|
284
|
+
Pass an explicit path to control where it lands — useful when you
|
|
285
|
+
want to write to a temp directory before uploading.
|
|
286
|
+
|
|
287
|
+
Returns
|
|
288
|
+
-------
|
|
289
|
+
PackageResult
|
|
290
|
+
Contains ``zip_path`` (a :class:`~pathlib.Path`), ``file_count``,
|
|
291
|
+
``compressed_bytes``, ``uncompressed_bytes``, and ``skipped_in_zip``.
|
|
292
|
+
|
|
293
|
+
Raises
|
|
294
|
+
------
|
|
295
|
+
NoFilesError
|
|
296
|
+
If *collection* is empty (nothing to zip).
|
|
297
|
+
OSError
|
|
298
|
+
If the ZIP file cannot be written.
|
|
299
|
+
|
|
300
|
+
Example
|
|
301
|
+
-------
|
|
302
|
+
::
|
|
303
|
+
|
|
304
|
+
import tempfile
|
|
305
|
+
from contextzip import get_git_changes, create_zip
|
|
306
|
+
|
|
307
|
+
collection = get_git_changes()
|
|
308
|
+
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp:
|
|
309
|
+
pkg = create_zip(collection, output=tmp.name)
|
|
310
|
+
|
|
311
|
+
with open(pkg.zip_path, "rb") as f:
|
|
312
|
+
upload_to_s3(f.read())
|
|
313
|
+
|
|
314
|
+
print(f"Packed {pkg.file_count} files, {pkg.compressed_bytes} bytes compressed")
|
|
315
|
+
"""
|
|
316
|
+
if not collection.files:
|
|
317
|
+
raise NoFilesError(
|
|
318
|
+
"The FileCollection is empty — nothing to zip. "
|
|
319
|
+
"Check get_git_changes() or get_files() returned files."
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
resolve_result = ResolveResult(
|
|
323
|
+
included=collection.files,
|
|
324
|
+
skipped=collection.skipped,
|
|
325
|
+
large_files=collection.large_files,
|
|
326
|
+
binary_files=collection.binary_files,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
output_path = Path(output).resolve() if output is not None else None
|
|
330
|
+
|
|
331
|
+
return create_zip_silent(
|
|
332
|
+
resolve_result=resolve_result,
|
|
333
|
+
project_dir=collection.project_dir,
|
|
334
|
+
output_path=output_path,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def detect_ecosystem(
|
|
339
|
+
path: str | Path | None = None,
|
|
340
|
+
) -> DetectionResult:
|
|
341
|
+
"""
|
|
342
|
+
Detect the ecosystem(s) present in a project directory.
|
|
343
|
+
|
|
344
|
+
Parameters
|
|
345
|
+
----------
|
|
346
|
+
path:
|
|
347
|
+
Directory to inspect. Defaults to ``Path.cwd()``.
|
|
348
|
+
|
|
349
|
+
Returns
|
|
350
|
+
-------
|
|
351
|
+
DetectionResult
|
|
352
|
+
Has ``.ecosystems`` (list of strings like ``["Next.js", "Node.js"]``),
|
|
353
|
+
``.display_name`` (e.g. ``"Next.js + Node.js"``), and
|
|
354
|
+
``.confidence`` (``"low"`` / ``"medium"`` / ``"high"``).
|
|
355
|
+
|
|
356
|
+
Example
|
|
357
|
+
-------
|
|
358
|
+
::
|
|
359
|
+
|
|
360
|
+
from contextzip import detect_ecosystem
|
|
361
|
+
|
|
362
|
+
result = detect_ecosystem("/path/to/project")
|
|
363
|
+
print(result.display_name) # "Django + Python"
|
|
364
|
+
print(result.confidence) # "high"
|
|
365
|
+
"""
|
|
366
|
+
return detect(_resolve_dir(path))
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
# ---------------------------------------------------------------------------
|
|
370
|
+
# Internal helpers
|
|
371
|
+
# ---------------------------------------------------------------------------
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _resolve_dir(path: str | Path | None) -> Path:
|
|
375
|
+
if path is None:
|
|
376
|
+
return Path(os.getcwd()).resolve()
|
|
377
|
+
return Path(path).resolve()
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _resolve_result_to_collection(
|
|
381
|
+
resolved: ResolveResult,
|
|
382
|
+
project_dir: Path,
|
|
383
|
+
detection: DetectionResult,
|
|
384
|
+
) -> FileCollection:
|
|
385
|
+
return FileCollection(
|
|
386
|
+
files=resolved.included,
|
|
387
|
+
skipped=resolved.skipped,
|
|
388
|
+
large_files=resolved.large_files,
|
|
389
|
+
binary_files=resolved.binary_files,
|
|
390
|
+
project_dir=project_dir,
|
|
391
|
+
ecosystem=detection.display_name,
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _raise_git_error(error: GitError) -> None:
|
|
396
|
+
if error.kind == GitErrorKind.GIT_NOT_FOUND:
|
|
397
|
+
raise GitNotFoundError(error.message)
|
|
398
|
+
if error.kind == GitErrorKind.NOT_A_REPO:
|
|
399
|
+
raise NotARepositoryError(error.message)
|
|
400
|
+
raise GitCommandError(error.message)
|