py-lean-package 0.1.1__py3-none-any.whl
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.
- py_lean_package/__init__.py +5 -0
- py_lean_package/analyzer.py +279 -0
- py_lean_package/cli.py +358 -0
- py_lean_package/models.py +110 -0
- py_lean_package-0.1.1.dist-info/METADATA +183 -0
- py_lean_package-0.1.1.dist-info/RECORD +8 -0
- py_lean_package-0.1.1.dist-info/WHEEL +4 -0
- py_lean_package-0.1.1.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""Core analysis logic for py-lean."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import fnmatch
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import tomlkit
|
|
10
|
+
|
|
11
|
+
from py_lean_package.models import AnalysisResult, Issue, IssueType, LeanConfig
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def read_gitignore(project_root: Path) -> list[str]:
|
|
15
|
+
"""Parse a local `.gitignore` file and return normalized ignore patterns.
|
|
16
|
+
|
|
17
|
+
Returns an empty list if the file does not exist or cannot be read.
|
|
18
|
+
Skips blank lines, comments (lines starting with `#`), and negation patterns (starting with `!`).
|
|
19
|
+
Normalizes patterns by stripping leading `/` (which anchors to the repo root) and trailing `/`
|
|
20
|
+
(which indicates a directory-only match).
|
|
21
|
+
"""
|
|
22
|
+
gitignore_path = project_root / ".gitignore"
|
|
23
|
+
try:
|
|
24
|
+
content = gitignore_path.read_text(encoding="utf-8")
|
|
25
|
+
except (FileNotFoundError, OSError):
|
|
26
|
+
return []
|
|
27
|
+
|
|
28
|
+
patterns: list[str] = []
|
|
29
|
+
for line in content.splitlines():
|
|
30
|
+
line = line.strip()
|
|
31
|
+
# Skip empty lines, comments, and negation patterns
|
|
32
|
+
if not line or line.startswith("#") or line.startswith("!"):
|
|
33
|
+
continue
|
|
34
|
+
# Normalize: strip leading `/` (root anchor) and trailing `/` (directory indicator)
|
|
35
|
+
line = line.removeprefix("/").removesuffix("/")
|
|
36
|
+
if line:
|
|
37
|
+
patterns.append(line)
|
|
38
|
+
return patterns
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def read_global_gitignore() -> list[str]:
|
|
42
|
+
"""Read the global gitignore file configured via `core.excludesFile`.
|
|
43
|
+
|
|
44
|
+
Returns an empty list if the config is not set or the file cannot be read.
|
|
45
|
+
Uses the same normalization as `read_gitignore`.
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
result = subprocess.run(
|
|
49
|
+
["git", "config", "--get", "core.excludesFile"],
|
|
50
|
+
capture_output=True,
|
|
51
|
+
text=True,
|
|
52
|
+
timeout=5,
|
|
53
|
+
)
|
|
54
|
+
if result.returncode != 0:
|
|
55
|
+
return []
|
|
56
|
+
excludes_file = result.stdout.strip()
|
|
57
|
+
if not excludes_file:
|
|
58
|
+
return []
|
|
59
|
+
|
|
60
|
+
content = Path(excludes_file).expanduser().read_text(encoding="utf-8")
|
|
61
|
+
except (FileNotFoundError, OSError, subprocess.TimeoutExpired):
|
|
62
|
+
return []
|
|
63
|
+
|
|
64
|
+
patterns: list[str] = []
|
|
65
|
+
for line in content.splitlines():
|
|
66
|
+
line = line.strip()
|
|
67
|
+
if not line or line.startswith("#") or line.startswith("!"):
|
|
68
|
+
continue
|
|
69
|
+
line = line.removeprefix("/").removesuffix("/")
|
|
70
|
+
if line:
|
|
71
|
+
patterns.append(line)
|
|
72
|
+
return patterns
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def is_gitignored(pattern: str, gitignore_entries: list[str]) -> bool:
|
|
76
|
+
"""Check if a pattern is already covered by a .gitignore entry.
|
|
77
|
+
|
|
78
|
+
Uses simple string matching and basic glob matching.
|
|
79
|
+
"""
|
|
80
|
+
pattern = pattern.rstrip("/")
|
|
81
|
+
for entry in gitignore_entries:
|
|
82
|
+
# Direct match
|
|
83
|
+
if entry == pattern:
|
|
84
|
+
return True
|
|
85
|
+
# Glob-like match
|
|
86
|
+
if fnmatch.fnmatch(pattern, entry):
|
|
87
|
+
return True
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def load_manifest(manifest_path: Path) -> tomlkit.TOMLDocument:
|
|
92
|
+
"""Load and parse a pyproject.toml file."""
|
|
93
|
+
if not manifest_path.exists():
|
|
94
|
+
raise FileNotFoundError(f"Manifest not found: {manifest_path}")
|
|
95
|
+
content = manifest_path.read_text(encoding="utf-8")
|
|
96
|
+
return tomlkit.parse(content)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def check_build_system(manifest: tomlkit.TOMLDocument) -> bool:
|
|
100
|
+
"""Verify the project uses uv_build as its build backend.
|
|
101
|
+
|
|
102
|
+
Checks for [build-system] with `requires` containing `uv_build` and
|
|
103
|
+
`build-backend` set to `uv_build.build`.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
True if the build system is correctly configured for uv_build.
|
|
107
|
+
"""
|
|
108
|
+
build_system = manifest.get("build-system", {}) or {}
|
|
109
|
+
requires = build_system.get("requires", []) or []
|
|
110
|
+
backend = build_system.get("build-backend", "")
|
|
111
|
+
|
|
112
|
+
has_uv_build_requires = any("uv_build" in str(r) for r in requires)
|
|
113
|
+
has_uv_build_backend = str(backend) == "uv_build.build"
|
|
114
|
+
|
|
115
|
+
return has_uv_build_requires and has_uv_build_backend
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def load_config(
|
|
119
|
+
manifest: tomlkit.TOMLDocument | None = None,
|
|
120
|
+
patterns: list[str] | None = None,
|
|
121
|
+
) -> LeanConfig:
|
|
122
|
+
"""Load py-lean configuration with the following precedence:
|
|
123
|
+
|
|
124
|
+
1. CLI --patterns (highest priority — completely replaces other sources)
|
|
125
|
+
2. tool.uv.build-backend.source-exclude from pyproject.toml
|
|
126
|
+
3. DEFAULT_SOURCE_EXCLUDE_PATTERNS (lowest priority — used when nothing is configured)
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
manifest: Parsed pyproject.toml document to read source-exclude from.
|
|
130
|
+
patterns: CLI-provided patterns that override all other sources.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
LeanConfig with patterns set according to the precedence rules above.
|
|
134
|
+
"""
|
|
135
|
+
config = LeanConfig()
|
|
136
|
+
|
|
137
|
+
# If CLI patterns are provided, they take full precedence
|
|
138
|
+
if patterns is not None:
|
|
139
|
+
config.patterns = patterns
|
|
140
|
+
return config
|
|
141
|
+
|
|
142
|
+
# Otherwise, try to load from tool.uv.build-backend.source-exclude
|
|
143
|
+
if manifest is not None:
|
|
144
|
+
source_exclude = _get_source_exclude_patterns(manifest)
|
|
145
|
+
if source_exclude:
|
|
146
|
+
config.patterns = source_exclude
|
|
147
|
+
|
|
148
|
+
# If neither CLI nor source-exclude provides patterns, effective_patterns()
|
|
149
|
+
# will fall back to DEFAULT_SOURCE_EXCLUDE_PATTERNS
|
|
150
|
+
return config
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _get_source_exclude_patterns(manifest: tomlkit.TOMLDocument) -> list[str]:
|
|
154
|
+
"""Extract source-exclude patterns from tool.uv.build-backend.source-exclude."""
|
|
155
|
+
patterns: list[str] = []
|
|
156
|
+
tool = manifest.get("tool", {})
|
|
157
|
+
if tool is None:
|
|
158
|
+
return patterns
|
|
159
|
+
uv = tool.get("uv", {})
|
|
160
|
+
if uv is None:
|
|
161
|
+
return patterns
|
|
162
|
+
build_backend = uv.get("build-backend", {})
|
|
163
|
+
if build_backend is None:
|
|
164
|
+
return patterns
|
|
165
|
+
source_exclude = build_backend.get("source-exclude", [])
|
|
166
|
+
if source_exclude is not None:
|
|
167
|
+
patterns = [str(p) for p in source_exclude]
|
|
168
|
+
return patterns
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _pattern_covers(pattern: str, exclude_pattern: str) -> bool:
|
|
172
|
+
"""Check if a source-exclude pattern covers a given artifact pattern.
|
|
173
|
+
|
|
174
|
+
Both patterns are treated as glob patterns. The exclude_pattern must match
|
|
175
|
+
the artifact pattern or be a parent directory of it.
|
|
176
|
+
"""
|
|
177
|
+
# Direct match
|
|
178
|
+
if fnmatch.fnmatch(pattern, exclude_pattern):
|
|
179
|
+
return True
|
|
180
|
+
|
|
181
|
+
# Check if the exclude pattern is a parent directory
|
|
182
|
+
# e.g., exclude "tests" covers "tests/**"
|
|
183
|
+
if pattern.startswith(exclude_pattern + "/"):
|
|
184
|
+
return True
|
|
185
|
+
|
|
186
|
+
# Check if the pattern's top-level component matches
|
|
187
|
+
pattern_top = pattern.split("/")[0]
|
|
188
|
+
exclude_top = exclude_pattern.split("/")[0]
|
|
189
|
+
return bool(fnmatch.fnmatch(pattern_top, exclude_top))
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def detect_missing_source_exclude(
|
|
193
|
+
manifest: tomlkit.TOMLDocument,
|
|
194
|
+
project_root: Path,
|
|
195
|
+
config: LeanConfig,
|
|
196
|
+
gitignore_entries: list[str] | None = None,
|
|
197
|
+
) -> list[Issue]:
|
|
198
|
+
"""Detect common repository artefacts that exist but are not in source-exclude.
|
|
199
|
+
|
|
200
|
+
Checks if artefacts listed in the config's patterns (or defaults) exist in the
|
|
201
|
+
project root but are not covered by tool.uv.build-backend.source-exclude.
|
|
202
|
+
Artefacts already covered by .gitignore entries are skipped.
|
|
203
|
+
"""
|
|
204
|
+
if gitignore_entries is None:
|
|
205
|
+
gitignore_entries = []
|
|
206
|
+
|
|
207
|
+
issues: list[Issue] = []
|
|
208
|
+
exclude_patterns = _get_source_exclude_patterns(manifest)
|
|
209
|
+
patterns_to_check = config.effective_patterns()
|
|
210
|
+
|
|
211
|
+
for pattern in patterns_to_check:
|
|
212
|
+
# Check if this pattern exists in the project root
|
|
213
|
+
if not _artifact_exists(project_root, pattern):
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
# Skip if already covered by .gitignore
|
|
217
|
+
if is_gitignored(pattern, gitignore_entries):
|
|
218
|
+
continue
|
|
219
|
+
|
|
220
|
+
# Check if it's covered by any source-exclude pattern
|
|
221
|
+
covered = any(_pattern_covers(pattern, ep) for ep in exclude_patterns)
|
|
222
|
+
if not covered:
|
|
223
|
+
issues.append(
|
|
224
|
+
Issue(
|
|
225
|
+
issue_type=IssueType.MISSING_SOURCE_EXCLUDE,
|
|
226
|
+
subject=pattern,
|
|
227
|
+
description=(
|
|
228
|
+
f"Artefact '{pattern}' exists in the project root but is not listed in "
|
|
229
|
+
f"tool.uv.build-backend.source-exclude."
|
|
230
|
+
),
|
|
231
|
+
location="pyproject.toml",
|
|
232
|
+
suggestion=f"Add '{pattern}' to tool.uv.build-backend.source-exclude.",
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
return issues
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _artifact_exists(project_root: Path, pattern: str) -> bool:
|
|
240
|
+
"""Check if an artifact matching the pattern exists in the project root."""
|
|
241
|
+
# Handle glob patterns like *.egg-info, *.pyc, *.pyo
|
|
242
|
+
if "*" in pattern:
|
|
243
|
+
matches = list(project_root.glob(pattern))
|
|
244
|
+
return len(matches) > 0
|
|
245
|
+
|
|
246
|
+
# Handle directory/file patterns
|
|
247
|
+
path = project_root / pattern
|
|
248
|
+
return path.exists()
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def analyze(
|
|
252
|
+
manifest_path: Path,
|
|
253
|
+
patterns: list[str] | None = None,
|
|
254
|
+
) -> AnalysisResult:
|
|
255
|
+
"""Run full analysis on a project.
|
|
256
|
+
|
|
257
|
+
Checks for missing source-exclude patterns for common repository artefacts.
|
|
258
|
+
Artefacts already covered by .gitignore (local or global) are skipped.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
manifest_path: Path to the pyproject.toml file.
|
|
262
|
+
patterns: Optional list of artefact patterns to check for source-exclude.
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
AnalysisResult containing all detected issues.
|
|
266
|
+
"""
|
|
267
|
+
project_root = manifest_path.parent
|
|
268
|
+
manifest = load_manifest(manifest_path)
|
|
269
|
+
config = load_config(manifest=manifest, patterns=patterns)
|
|
270
|
+
|
|
271
|
+
# Collect gitignore entries from local .gitignore and global excludesFile
|
|
272
|
+
gitignore_entries: list[str] = []
|
|
273
|
+
gitignore_entries.extend(read_gitignore(project_root))
|
|
274
|
+
gitignore_entries.extend(read_global_gitignore())
|
|
275
|
+
|
|
276
|
+
issues: list[Issue] = []
|
|
277
|
+
issues.extend(detect_missing_source_exclude(manifest, project_root, config, gitignore_entries))
|
|
278
|
+
|
|
279
|
+
return AnalysisResult(manifest_path=manifest_path, issues=issues)
|
py_lean_package/cli.py
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"""CLI entry point for py-lean."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from difflib import unified_diff
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import tomlkit
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from py_lean_package import __version__
|
|
13
|
+
from py_lean_package.analyzer import _get_source_exclude_patterns, analyze, check_build_system, load_manifest
|
|
14
|
+
from py_lean_package.models import DEFAULT_SOURCE_EXCLUDE_PATTERNS, IssueType, OutputFormat
|
|
15
|
+
|
|
16
|
+
app = typer.Typer(
|
|
17
|
+
name="py-lean",
|
|
18
|
+
help="Detect and fix missing source-exclude patterns in Python projects.",
|
|
19
|
+
add_completion=False,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Common options
|
|
24
|
+
def manifest_path_callback(ctx: typer.Context, value: str) -> Path:
|
|
25
|
+
"""Validate and resolve the manifest path."""
|
|
26
|
+
path = Path(value)
|
|
27
|
+
if not path.exists():
|
|
28
|
+
raise typer.Exit(1)
|
|
29
|
+
return path.resolve()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
#
|
|
33
|
+
# Commands
|
|
34
|
+
#
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@app.command("lint")
|
|
38
|
+
def cmd_lint(
|
|
39
|
+
manifest_path: Path = typer.Option(
|
|
40
|
+
"pyproject.toml",
|
|
41
|
+
"-m",
|
|
42
|
+
"--manifest-path",
|
|
43
|
+
callback=manifest_path_callback,
|
|
44
|
+
help="Path to the pyproject.toml file.",
|
|
45
|
+
),
|
|
46
|
+
format: OutputFormat = typer.Option(
|
|
47
|
+
OutputFormat.TEXT,
|
|
48
|
+
"--format",
|
|
49
|
+
help="Output format: text or json.",
|
|
50
|
+
),
|
|
51
|
+
patterns: list[str] | None = typer.Option(
|
|
52
|
+
None,
|
|
53
|
+
"-p",
|
|
54
|
+
"--patterns",
|
|
55
|
+
help="Artefact patterns to check for source-exclude (e.g., tests).",
|
|
56
|
+
),
|
|
57
|
+
dry_run: bool = typer.Option(
|
|
58
|
+
False,
|
|
59
|
+
"--dry-run",
|
|
60
|
+
help="Show what would be done without making changes.",
|
|
61
|
+
),
|
|
62
|
+
diff: bool = typer.Option(
|
|
63
|
+
False,
|
|
64
|
+
"--diff",
|
|
65
|
+
help="Show a diff of the changes that would be made.",
|
|
66
|
+
),
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Analyze a project for missing source-exclude patterns."""
|
|
69
|
+
manifest = load_manifest(manifest_path)
|
|
70
|
+
if not check_build_system(manifest):
|
|
71
|
+
typer.echo(
|
|
72
|
+
"Error: This project is not configured with uv_build as its build backend.\n\n"
|
|
73
|
+
"py-lean requires the following in pyproject.toml:\n\n"
|
|
74
|
+
" [build-system]\n"
|
|
75
|
+
' requires = ["uv_build"]\n'
|
|
76
|
+
' build-backend = "uv_build.build"',
|
|
77
|
+
err=True,
|
|
78
|
+
)
|
|
79
|
+
raise typer.Exit(1)
|
|
80
|
+
result = analyze(manifest_path, patterns)
|
|
81
|
+
|
|
82
|
+
if format == OutputFormat.JSON:
|
|
83
|
+
typer.echo(json.dumps(result.to_dict(), indent=2))
|
|
84
|
+
else:
|
|
85
|
+
if result.is_clean:
|
|
86
|
+
typer.echo("✓ No issues found.")
|
|
87
|
+
else:
|
|
88
|
+
_print_issues(result.issues)
|
|
89
|
+
typer.echo(f"\nFound {len(result.issues)} issue(s).", err=True)
|
|
90
|
+
raise typer.Exit(1)
|
|
91
|
+
|
|
92
|
+
if dry_run:
|
|
93
|
+
typer.echo("(dry-run: no changes made)")
|
|
94
|
+
if diff:
|
|
95
|
+
_show_diff(manifest_path, result.issues)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@app.command("create")
|
|
99
|
+
def cmd_create(
|
|
100
|
+
manifest_path: Path = typer.Option(
|
|
101
|
+
"pyproject.toml",
|
|
102
|
+
"-m",
|
|
103
|
+
"--manifest-path",
|
|
104
|
+
callback=manifest_path_callback,
|
|
105
|
+
help="Path to the pyproject.toml file.",
|
|
106
|
+
),
|
|
107
|
+
dry_run: bool = typer.Option(
|
|
108
|
+
False,
|
|
109
|
+
"--dry-run",
|
|
110
|
+
help="Show what would be done without making changes.",
|
|
111
|
+
),
|
|
112
|
+
diff: bool = typer.Option(
|
|
113
|
+
False,
|
|
114
|
+
"--diff",
|
|
115
|
+
help="Show a diff of the changes that would be made.",
|
|
116
|
+
),
|
|
117
|
+
) -> None:
|
|
118
|
+
"""Add source-exclude patterns for repository artefacts to tool.uv.build-backend in pyproject.toml.
|
|
119
|
+
|
|
120
|
+
Merges configured patterns with built-in defaults and validates against the Git repository.
|
|
121
|
+
Only adds patterns for artefacts that actually exist in the project.
|
|
122
|
+
"""
|
|
123
|
+
manifest = load_manifest(manifest_path)
|
|
124
|
+
if not check_build_system(manifest):
|
|
125
|
+
typer.echo(
|
|
126
|
+
"Error: This project is not configured with uv_build as its build backend.\n\n"
|
|
127
|
+
"py-lean requires the following in pyproject.toml:\n\n"
|
|
128
|
+
" [build-system]\n"
|
|
129
|
+
' requires = ["uv_build"]\n'
|
|
130
|
+
' build-backend = "uv_build.build"',
|
|
131
|
+
err=True,
|
|
132
|
+
)
|
|
133
|
+
raise typer.Exit(1)
|
|
134
|
+
# Merge configured patterns with built-in defaults
|
|
135
|
+
manifest = tomlkit.parse(manifest_path.read_text(encoding="utf-8"))
|
|
136
|
+
current_excludes = _get_source_exclude_patterns(manifest)
|
|
137
|
+
merged_patterns = list(current_excludes)
|
|
138
|
+
for pattern in DEFAULT_SOURCE_EXCLUDE_PATTERNS:
|
|
139
|
+
if pattern not in merged_patterns:
|
|
140
|
+
merged_patterns.append(pattern)
|
|
141
|
+
|
|
142
|
+
# Analyze with merged patterns to find which artefacts exist in the repo
|
|
143
|
+
result = analyze(manifest_path, merged_patterns)
|
|
144
|
+
|
|
145
|
+
if result.is_clean:
|
|
146
|
+
typer.echo("✓ No missing source-exclude patterns to add.")
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
original_content = manifest_path.read_text(encoding="utf-8")
|
|
150
|
+
manifest = tomlkit.parse(original_content)
|
|
151
|
+
|
|
152
|
+
# Ensure tool.uv.build-backend section exists
|
|
153
|
+
if "tool" not in manifest:
|
|
154
|
+
manifest["tool"] = {}
|
|
155
|
+
if "uv" not in manifest["tool"]:
|
|
156
|
+
manifest["tool"]["uv"] = {}
|
|
157
|
+
if "build-backend" not in manifest["tool"]["uv"]:
|
|
158
|
+
manifest["tool"]["uv"]["build-backend"] = {}
|
|
159
|
+
|
|
160
|
+
bb = manifest["tool"]["uv"]["build-backend"]
|
|
161
|
+
current = list(bb.get("source-exclude", []))
|
|
162
|
+
|
|
163
|
+
# Add only the missing patterns that exist in the repo
|
|
164
|
+
added = 0
|
|
165
|
+
for issue in result.issues:
|
|
166
|
+
if issue.issue_type == IssueType.MISSING_SOURCE_EXCLUDE:
|
|
167
|
+
pattern = issue.subject
|
|
168
|
+
if pattern not in current:
|
|
169
|
+
current.append(pattern)
|
|
170
|
+
added += 1
|
|
171
|
+
|
|
172
|
+
bb["source-exclude"] = current
|
|
173
|
+
new_content = tomlkit.dumps(manifest)
|
|
174
|
+
|
|
175
|
+
if dry_run or diff:
|
|
176
|
+
if diff:
|
|
177
|
+
_print_diff(original_content, new_content, str(manifest_path))
|
|
178
|
+
if dry_run:
|
|
179
|
+
typer.echo("(dry-run: no changes made)")
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
manifest_path.write_text(new_content, encoding="utf-8")
|
|
183
|
+
typer.echo(f"Added {added} source-exclude pattern(s) to {manifest_path}")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@app.command("update")
|
|
187
|
+
def cmd_update(
|
|
188
|
+
manifest_path: Path = typer.Option(
|
|
189
|
+
"pyproject.toml",
|
|
190
|
+
"-m",
|
|
191
|
+
"--manifest-path",
|
|
192
|
+
callback=manifest_path_callback,
|
|
193
|
+
help="Path to the pyproject.toml file.",
|
|
194
|
+
),
|
|
195
|
+
dry_run: bool = typer.Option(
|
|
196
|
+
False,
|
|
197
|
+
"--dry-run",
|
|
198
|
+
help="Show what would be done without making changes.",
|
|
199
|
+
),
|
|
200
|
+
diff: bool = typer.Option(
|
|
201
|
+
False,
|
|
202
|
+
"--diff",
|
|
203
|
+
help="Show a diff of the changes that would be made.",
|
|
204
|
+
),
|
|
205
|
+
) -> None:
|
|
206
|
+
"""Merge configured patterns with built-in defaults and update source-exclude in pyproject.toml.
|
|
207
|
+
|
|
208
|
+
Validates against the Git repository — only adds patterns for artefacts that actually exist.
|
|
209
|
+
"""
|
|
210
|
+
manifest = load_manifest(manifest_path)
|
|
211
|
+
if not check_build_system(manifest):
|
|
212
|
+
typer.echo(
|
|
213
|
+
"Error: This project is not configured with uv_build as its build backend.\n\n"
|
|
214
|
+
"py-lean requires the following in pyproject.toml:\n\n"
|
|
215
|
+
" [build-system]\n"
|
|
216
|
+
' requires = ["uv_build"]\n'
|
|
217
|
+
' build-backend = "uv_build.build"',
|
|
218
|
+
err=True,
|
|
219
|
+
)
|
|
220
|
+
raise typer.Exit(1)
|
|
221
|
+
# Merge configured patterns with built-in defaults
|
|
222
|
+
manifest = tomlkit.parse(manifest_path.read_text(encoding="utf-8"))
|
|
223
|
+
current_excludes = _get_source_exclude_patterns(manifest)
|
|
224
|
+
merged_patterns = list(current_excludes)
|
|
225
|
+
for pattern in DEFAULT_SOURCE_EXCLUDE_PATTERNS:
|
|
226
|
+
if pattern not in merged_patterns:
|
|
227
|
+
merged_patterns.append(pattern)
|
|
228
|
+
|
|
229
|
+
# Analyze with merged patterns to find which artefacts exist in the repo
|
|
230
|
+
result = analyze(manifest_path, merged_patterns)
|
|
231
|
+
|
|
232
|
+
if result.is_clean:
|
|
233
|
+
typer.echo("✓ No missing source-exclude patterns to add.")
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
original_content = manifest_path.read_text(encoding="utf-8")
|
|
237
|
+
manifest = tomlkit.parse(original_content)
|
|
238
|
+
|
|
239
|
+
# Ensure tool.uv.build-backend section exists
|
|
240
|
+
if "tool" not in manifest:
|
|
241
|
+
manifest["tool"] = {}
|
|
242
|
+
if "uv" not in manifest["tool"]:
|
|
243
|
+
manifest["tool"]["uv"] = {}
|
|
244
|
+
if "build-backend" not in manifest["tool"]["uv"]:
|
|
245
|
+
manifest["tool"]["uv"]["build-backend"] = {}
|
|
246
|
+
|
|
247
|
+
bb = manifest["tool"]["uv"]["build-backend"]
|
|
248
|
+
current = list(bb.get("source-exclude", []))
|
|
249
|
+
|
|
250
|
+
# Add only the missing patterns that exist in the repo
|
|
251
|
+
added = 0
|
|
252
|
+
for issue in result.issues:
|
|
253
|
+
if issue.issue_type == IssueType.MISSING_SOURCE_EXCLUDE:
|
|
254
|
+
pattern = issue.subject
|
|
255
|
+
if pattern not in current:
|
|
256
|
+
current.append(pattern)
|
|
257
|
+
added += 1
|
|
258
|
+
|
|
259
|
+
bb["source-exclude"] = current
|
|
260
|
+
new_content = tomlkit.dumps(manifest)
|
|
261
|
+
|
|
262
|
+
if dry_run or diff:
|
|
263
|
+
if diff:
|
|
264
|
+
_print_diff(original_content, new_content, str(manifest_path))
|
|
265
|
+
if dry_run:
|
|
266
|
+
typer.echo("(dry-run: no changes made)")
|
|
267
|
+
return
|
|
268
|
+
|
|
269
|
+
manifest_path.write_text(new_content, encoding="utf-8")
|
|
270
|
+
typer.echo(f"Added {added} source-exclude pattern(s) to {manifest_path}")
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
#
|
|
274
|
+
# Helpers
|
|
275
|
+
#
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _print_issues(issues: list) -> None:
|
|
279
|
+
"""Print issues in a human-readable format."""
|
|
280
|
+
for issue in issues:
|
|
281
|
+
icon = _issue_icon(issue.issue_type)
|
|
282
|
+
typer.echo(f"{icon} {issue.subject}")
|
|
283
|
+
typer.echo(f" {issue.description}")
|
|
284
|
+
if issue.suggestion:
|
|
285
|
+
typer.echo(f" → {issue.suggestion}")
|
|
286
|
+
typer.echo()
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _issue_icon(issue_type: IssueType) -> str:
|
|
290
|
+
"""Get a display icon for an issue type."""
|
|
291
|
+
icons = {
|
|
292
|
+
IssueType.MISSING_SOURCE_EXCLUDE: "✕",
|
|
293
|
+
}
|
|
294
|
+
return icons.get(issue_type, "?")
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _print_diff(old: str, new: str, path: str) -> None:
|
|
298
|
+
"""Print a unified diff between old and new content."""
|
|
299
|
+
old_lines = old.splitlines(keepends=True)
|
|
300
|
+
new_lines = new.splitlines(keepends=True)
|
|
301
|
+
|
|
302
|
+
diff_lines = unified_diff(old_lines, new_lines, fromfile=f"a/{path}", tofile=f"b/{path}")
|
|
303
|
+
for line in diff_lines:
|
|
304
|
+
typer.echo(line, nl=False)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _show_diff(manifest_path: Path, issues: list) -> None:
|
|
308
|
+
"""Show what changes would be made to fix the issues."""
|
|
309
|
+
original_content = manifest_path.read_text(encoding="utf-8")
|
|
310
|
+
manifest = tomlkit.parse(original_content)
|
|
311
|
+
|
|
312
|
+
# Handle missing source-exclude
|
|
313
|
+
missing_excludes = [i for i in issues if i.issue_type == IssueType.MISSING_SOURCE_EXCLUDE]
|
|
314
|
+
if missing_excludes:
|
|
315
|
+
if "tool" not in manifest:
|
|
316
|
+
manifest["tool"] = {}
|
|
317
|
+
if "uv" not in manifest["tool"]:
|
|
318
|
+
manifest["tool"]["uv"] = {}
|
|
319
|
+
if "build-backend" not in manifest["tool"]["uv"]:
|
|
320
|
+
manifest["tool"]["uv"]["build-backend"] = {}
|
|
321
|
+
|
|
322
|
+
bb = manifest["tool"]["uv"]["build-backend"]
|
|
323
|
+
current_excludes = list(bb.get("source-exclude", []))
|
|
324
|
+
for issue in missing_excludes:
|
|
325
|
+
if issue.subject not in current_excludes:
|
|
326
|
+
current_excludes.append(issue.subject)
|
|
327
|
+
if current_excludes:
|
|
328
|
+
bb["source-exclude"] = current_excludes
|
|
329
|
+
|
|
330
|
+
new_content = tomlkit.dumps(manifest)
|
|
331
|
+
if new_content != original_content:
|
|
332
|
+
_print_diff(original_content, new_content, str(manifest_path))
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def version_callback(value: bool) -> None:
|
|
336
|
+
"""Print the version and exit."""
|
|
337
|
+
if value:
|
|
338
|
+
typer.echo(f"py-lean {__version__}")
|
|
339
|
+
raise typer.Exit()
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
@app.callback()
|
|
343
|
+
def main(
|
|
344
|
+
_version: bool = typer.Option(
|
|
345
|
+
False,
|
|
346
|
+
"--version",
|
|
347
|
+
"-V",
|
|
348
|
+
callback=version_callback,
|
|
349
|
+
is_eager=True,
|
|
350
|
+
help="Show the version and exit.",
|
|
351
|
+
),
|
|
352
|
+
) -> None:
|
|
353
|
+
"""py-lean: Detect and fix missing source-exclude patterns in Python projects."""
|
|
354
|
+
pass
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
if __name__ == "__main__":
|
|
358
|
+
app()
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Data models for py-lean."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class IssueType(StrEnum):
|
|
11
|
+
"""Types of issues that py-lean can detect."""
|
|
12
|
+
|
|
13
|
+
MISSING_SOURCE_EXCLUDE = "missing_source_exclude"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class OutputFormat(StrEnum):
|
|
17
|
+
"""Output format for py-lean results."""
|
|
18
|
+
|
|
19
|
+
TEXT = "text"
|
|
20
|
+
JSON = "json"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class Issue:
|
|
25
|
+
"""A single issue detected by py-lean."""
|
|
26
|
+
|
|
27
|
+
issue_type: IssueType
|
|
28
|
+
subject: str
|
|
29
|
+
description: str
|
|
30
|
+
location: str | None = None
|
|
31
|
+
suggestion: str | None = None
|
|
32
|
+
|
|
33
|
+
def to_dict(self) -> dict:
|
|
34
|
+
"""Convert the issue to a dictionary for JSON serialization."""
|
|
35
|
+
result: dict = {
|
|
36
|
+
"type": self.issue_type.value,
|
|
37
|
+
"subject": self.subject,
|
|
38
|
+
"description": self.description,
|
|
39
|
+
}
|
|
40
|
+
if self.location:
|
|
41
|
+
result["location"] = self.location
|
|
42
|
+
if self.suggestion:
|
|
43
|
+
result["suggestion"] = self.suggestion
|
|
44
|
+
return result
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class AnalysisResult:
|
|
49
|
+
"""Result of analyzing a project."""
|
|
50
|
+
|
|
51
|
+
manifest_path: Path
|
|
52
|
+
issues: list[Issue] = field(default_factory=list)
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def is_clean(self) -> bool:
|
|
56
|
+
"""Return True if no issues were found."""
|
|
57
|
+
return len(self.issues) == 0
|
|
58
|
+
|
|
59
|
+
def to_dict(self) -> dict:
|
|
60
|
+
"""Convert the result to a dictionary for JSON serialization."""
|
|
61
|
+
return {
|
|
62
|
+
"manifest_path": str(self.manifest_path),
|
|
63
|
+
"issues": [issue.to_dict() for issue in self.issues],
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# Default artifact patterns that should typically be excluded from builds.
|
|
68
|
+
# These are common repository artefacts that should not end up in wheels or sdists.
|
|
69
|
+
DEFAULT_SOURCE_EXCLUDE_PATTERNS: list[str] = [
|
|
70
|
+
"tests",
|
|
71
|
+
"docs",
|
|
72
|
+
".venv",
|
|
73
|
+
"__pycache__",
|
|
74
|
+
"*.egg-info",
|
|
75
|
+
".tox",
|
|
76
|
+
".mypy_cache",
|
|
77
|
+
".pytest_cache",
|
|
78
|
+
"dist",
|
|
79
|
+
"build",
|
|
80
|
+
".github",
|
|
81
|
+
".idea",
|
|
82
|
+
".vscode",
|
|
83
|
+
".vs",
|
|
84
|
+
".DS_Store",
|
|
85
|
+
"*.pyc",
|
|
86
|
+
"*.pyo",
|
|
87
|
+
".coverage",
|
|
88
|
+
"htmlcov",
|
|
89
|
+
".ruff_cache",
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class LeanConfig:
|
|
95
|
+
"""Configuration for py-lean.
|
|
96
|
+
|
|
97
|
+
Patterns are determined by the following precedence:
|
|
98
|
+
1. CLI --patterns (highest priority)
|
|
99
|
+
2. tool.uv.build-backend.source-exclude from pyproject.toml
|
|
100
|
+
3. Built-in defaults (DEFAULT_SOURCE_EXCLUDE_PATTERNS)
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
# Source exclude patterns to check for (defaults to DEFAULT_SOURCE_EXCLUDE_PATTERNS)
|
|
104
|
+
patterns: list[str] = field(default_factory=list)
|
|
105
|
+
|
|
106
|
+
def effective_patterns(self) -> list[str]:
|
|
107
|
+
"""Return the patterns to use, falling back to defaults if none configured."""
|
|
108
|
+
if self.patterns:
|
|
109
|
+
return self.patterns
|
|
110
|
+
return list(DEFAULT_SOURCE_EXCLUDE_PATTERNS)
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: py-lean-package
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Detect and fix missing source-exclude patterns in Python projects.
|
|
5
|
+
Author: Raphael Stolt
|
|
6
|
+
Author-email: Raphael Stolt <raphael.stolt@gmail.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
Requires-Dist: pydantic>=2.14.1
|
|
9
|
+
Requires-Dist: tomlkit>=0.15.2
|
|
10
|
+
Requires-Dist: typer>=0.27.3
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# py-lean
|
|
15
|
+
|
|
16
|
+
Detect and fix missing `source-exclude` patterns in Python projects built with `uv_build`.
|
|
17
|
+
|
|
18
|
+
`py-lean` is a Python counterpart to [cargo-lean](https://github.com/raphaelstolt/cargo-lean). It checks the source-distribution configuration in
|
|
19
|
+
`pyproject.toml` so repository-only files don't unnecessarily end up in published archives.
|
|
20
|
+
|
|
21
|
+
## Why?
|
|
22
|
+
|
|
23
|
+
A Python package's source repository often contains more than consumers need to install and use it. Tests, CI workflows,
|
|
24
|
+
development configuration, and other repository-specific files can add unnecessary noise and size to source distributions.
|
|
25
|
+
|
|
26
|
+
`py-lean` helps make distribution contents intentional by identifying common repository artefacts that are missing from
|
|
27
|
+
`tool.uv.build-backend.source-exclude`. It can also add the missing patterns to `pyproject.toml`.
|
|
28
|
+
|
|
29
|
+
Your working repository remains intact: `py-lean` configures what is excluded from the built source distribution; it does
|
|
30
|
+
not delete those files from your project.
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
|
|
34
|
+
- __Lint__ — Detect common repository artefacts that are not listed in `source-exclude`.
|
|
35
|
+
- __Create__ — Add a `source-exclude` configuration for detected artefacts.
|
|
36
|
+
- __Update__ — Add missing exclusion patterns to an existing configuration.
|
|
37
|
+
- __Preview changes__ — Use `--dry-run` and `--diff` to review proposed edits before applying them.
|
|
38
|
+
- __Machine-readable output__ — Emit lint results as JSON.
|
|
39
|
+
|
|
40
|
+
## Requirements
|
|
41
|
+
|
|
42
|
+
- Python 3.11 or newer
|
|
43
|
+
- [uv](https://github.com/astral-sh/uv) and a project using the `uv_build` build backend
|
|
44
|
+
|
|
45
|
+
### Required build-system configuration
|
|
46
|
+
|
|
47
|
+
`py-lean` manages `tool.uv.build-backend.source-exclude`, which is a `uv_build` configuration. The target project must
|
|
48
|
+
therefore use `uv_build` as its build backend for this setting to apply.
|
|
49
|
+
|
|
50
|
+
For example, the target project's `pyproject.toml` should include:
|
|
51
|
+
|
|
52
|
+
```toml
|
|
53
|
+
[build-system]
|
|
54
|
+
requires = ["uv_build"]
|
|
55
|
+
build-backend = "uv_build"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
If the project uses a different build backend, `py-lean`'s `source-exclude` configuration will not control that backend's
|
|
59
|
+
source-distribution contents.
|
|
60
|
+
|
|
61
|
+
## Installation
|
|
62
|
+
|
|
63
|
+
Install with `uv`:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
uv tool install py-lean-package
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Or install from source:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
git clone https://github.com/raphaelstolt/py-lean.git
|
|
73
|
+
cd py-lean
|
|
74
|
+
uv sync
|
|
75
|
+
uv run --project . py-lean --help
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Usage
|
|
79
|
+
|
|
80
|
+
### Lint
|
|
81
|
+
|
|
82
|
+
Check for missing patterns in `source-exclude`:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
py-lean lint
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Options:
|
|
89
|
+
|
|
90
|
+
| Flag | Description |
|
|
91
|
+
|------|-------------|
|
|
92
|
+
| `-m, --manifest-path <PATH>` | Path to `pyproject.toml` (default: `pyproject.toml`) |
|
|
93
|
+
| `--format <text\|json>` | Output format (default: `text`) |
|
|
94
|
+
| `-p, --patterns <PATTERN>` | Artefact patterns to check, such as `tests/` |
|
|
95
|
+
| `--dry-run` | Show what would be done without making changes |
|
|
96
|
+
| `--diff` | Show a diff of the proposed changes |
|
|
97
|
+
|
|
98
|
+
Examples:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
# Lint with JSON output
|
|
102
|
+
py-lean lint --format json
|
|
103
|
+
|
|
104
|
+
# Use a custom manifest path
|
|
105
|
+
py-lean lint -m path/to/pyproject.toml
|
|
106
|
+
|
|
107
|
+
# Check selected artefact patterns
|
|
108
|
+
py-lean lint -p "tests/" -p "benchmarks/"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Create
|
|
112
|
+
|
|
113
|
+
Create a `tool.uv.build-backend.source-exclude` configuration containing common repository artefacts:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
py-lean create
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
This scans the project for common artefacts, such as `tests/`, `docs/`, and `.git/`, and adds the detected patterns to
|
|
120
|
+
`pyproject.toml`.
|
|
121
|
+
|
|
122
|
+
Options:
|
|
123
|
+
|
|
124
|
+
| Flag | Description |
|
|
125
|
+
|------|-------------|
|
|
126
|
+
| `--dry-run` | Preview changes without modifying files |
|
|
127
|
+
| `--diff` | Show a unified diff of the proposed changes |
|
|
128
|
+
|
|
129
|
+
### Update
|
|
130
|
+
|
|
131
|
+
Add missing patterns to an existing `source-exclude` configuration:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
py-lean update
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Options:
|
|
138
|
+
|
|
139
|
+
| Flag | Description |
|
|
140
|
+
|------|-------------|
|
|
141
|
+
| `--dry-run` | Preview changes without modifying files |
|
|
142
|
+
| `--diff` | Show a unified diff of the proposed changes |
|
|
143
|
+
|
|
144
|
+
Examples:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
# Preview changes
|
|
148
|
+
py-lean update --dry-run --diff
|
|
149
|
+
|
|
150
|
+
# Apply changes
|
|
151
|
+
py-lean update
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Configuration
|
|
155
|
+
|
|
156
|
+
### Pattern precedence
|
|
157
|
+
|
|
158
|
+
`py-lean` determines which artefact patterns to check using the following precedence:
|
|
159
|
+
|
|
160
|
+
1. __CLI `--patterns`__ — highest priority; replaces other pattern sources.
|
|
161
|
+
2. __`tool.uv.build-backend.source-exclude`__ in `pyproject.toml` — patterns already configured for the build backend.
|
|
162
|
+
3. __Built-in defaults__ — used when neither of the above is configured.
|
|
163
|
+
|
|
164
|
+
Use `--patterns` to override the default patterns:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
py-lean lint -p "tests/" -p "benchmarks/"
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
By default, `py-lean` checks a built-in set of common repository artefacts.
|
|
171
|
+
|
|
172
|
+
## Development
|
|
173
|
+
|
|
174
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for development instructions.
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
|
|
178
|
+
This project is licensed under the MIT License. See [LICENSE.md](LICENSE.md) for details.
|
|
179
|
+
|
|
180
|
+
## Related projects
|
|
181
|
+
|
|
182
|
+
- [lean-package-validator](https://github.com/raphaelstolt/lean-package-validator) — The `PHP` equivalent
|
|
183
|
+
- [cargo-lean](https://github.com/raphaelstolt/cargo-lean) — The `Rust` equivalent.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
py_lean_package/__init__.py,sha256=4K2dw7LHYennvwdzVp36kxb6ixPiVksxWQiSPEHMjhI,141
|
|
2
|
+
py_lean_package/analyzer.py,sha256=LHSmhdlIxLhLpjrF-TFnvPA7J7IS1wNW93NA9Qd0wmw,9763
|
|
3
|
+
py_lean_package/cli.py,sha256=D6y7ADdYLucP_I88uqg9YXJBqhOmQBPFXg7x2Zsawh4,11493
|
|
4
|
+
py_lean_package/models.py,sha256=C9XAmIUh776WGDmwzf_YAgjN37Yx_IvFFwgJQDVG24o,2785
|
|
5
|
+
py_lean_package-0.1.1.dist-info/WHEEL,sha256=3NO1oYsF7mANFUWVdyEQLM-zmecczWWEmmF5rEbXrWk,80
|
|
6
|
+
py_lean_package-0.1.1.dist-info/entry_points.txt,sha256=uwGGC1DALLEJC1th2ozkSqZRwLg5d66GtIFXnZLrXpY,53
|
|
7
|
+
py_lean_package-0.1.1.dist-info/METADATA,sha256=hismgKpevvtO66pArtC_HRQHf9bKFZIXnR4grHaFUT8,5230
|
|
8
|
+
py_lean_package-0.1.1.dist-info/RECORD,,
|