pygenkit 0.2.0__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.
- pygenkit/__init__.py +2 -0
- pygenkit/ai/__init__.py +11 -0
- pygenkit/ai/prompts.py +27 -0
- pygenkit/ai/provider.py +57 -0
- pygenkit/ai/review.py +59 -0
- pygenkit/cli/__init__.py +3 -0
- pygenkit/cli/app.py +53 -0
- pygenkit/cli/commands/__init__.py +21 -0
- pygenkit/cli/commands/build.py +41 -0
- pygenkit/cli/commands/doctor.py +119 -0
- pygenkit/cli/commands/generate.py +52 -0
- pygenkit/cli/commands/health.py +64 -0
- pygenkit/cli/commands/init.py +23 -0
- pygenkit/cli/commands/inspect.py +58 -0
- pygenkit/cli/commands/new.py +37 -0
- pygenkit/cli/commands/publish.py +30 -0
- pygenkit/cli/commands/release.py +33 -0
- pygenkit/cli/commands/release_check.py +17 -0
- pygenkit/cli/commands/review.py +58 -0
- pygenkit/cli/commands/validate.py +29 -0
- pygenkit/cli.py +4 -0
- pygenkit/config/__init__.py +3 -0
- pygenkit/generators/__init__.py +13 -0
- pygenkit/generators/base.py +31 -0
- pygenkit/generators/deploy.py +48 -0
- pygenkit/generators/docker.py +43 -0
- pygenkit/generators/github_actions.py +68 -0
- pygenkit/generators/orchestrator.py +33 -0
- pygenkit/generators/project.py +110 -0
- pygenkit/health/__init__.py +3 -0
- pygenkit/health/api.py +57 -0
- pygenkit/health/checks.py +343 -0
- pygenkit/inspector/__init__.py +3 -0
- pygenkit/inspector/api.py +154 -0
- pygenkit/inspector/debian.py +51 -0
- pygenkit/inspector/detect.py +84 -0
- pygenkit/inspector/git.py +55 -0
- pygenkit/inspector/pyproject.py +74 -0
- pygenkit/models/__init__.py +25 -0
- pygenkit/models/config.py +266 -0
- pygenkit/models/health.py +22 -0
- pygenkit/models/inspection.py +56 -0
- pygenkit/render/__init__.py +3 -0
- pygenkit/render/engine.py +41 -0
- pygenkit/services/__init__.py +0 -0
- pygenkit/templates/deploy/Procfile.j2 +1 -0
- pygenkit/templates/deploy/fly.toml.j2 +14 -0
- pygenkit/templates/deploy/railway.json.j2 +12 -0
- pygenkit/templates/docker/Dockerfile.j2 +15 -0
- pygenkit/templates/docker/docker-compose.yml.j2 +17 -0
- pygenkit/templates/github/workflows/ci.yml.j2 +33 -0
- pygenkit/templates/github/workflows/publish-launchpad.yml.j2 +37 -0
- pygenkit/templates/github/workflows/publish-pypi.yml.j2 +32 -0
- pygenkit/templates/github/workflows/release.yml.j2 +28 -0
- pygenkit/templates/project/LICENSE.j2 +21 -0
- pygenkit/templates/project/README.md.j2 +21 -0
- pygenkit/templates/project/pyproject.toml.j2 +51 -0
- pygenkit/templates/project/src/__init__.py.j2 +2 -0
- pygenkit/templates/project/src/cli.py.j2 +5 -0
- pygenkit/templates/project/tests/__init__.py.j2 +0 -0
- pygenkit/templates/project/tests/test_cli.py.j2 +5 -0
- pygenkit/templates/python-cli/CHANGELOG.md.jinja +7 -0
- pygenkit/templates/python-cli/LICENSE.jinja +204 -0
- pygenkit/templates/python-cli/Makefile.jinja +31 -0
- pygenkit/templates/python-cli/README.md.jinja +86 -0
- pygenkit/templates/python-cli/debian/changelog.jinja +5 -0
- pygenkit/templates/python-cli/debian/control.jinja +23 -0
- pygenkit/templates/python-cli/debian/copyright.jinja +23 -0
- pygenkit/templates/python-cli/debian/install.jinja +1 -0
- pygenkit/templates/python-cli/debian/links.jinja +1 -0
- pygenkit/templates/python-cli/debian/postinst.jinja +15 -0
- pygenkit/templates/python-cli/debian/prerm.jinja +14 -0
- pygenkit/templates/python-cli/debian/rules.jinja +11 -0
- pygenkit/templates/python-cli/debian/source/options.jinja +2 -0
- pygenkit/templates/python-cli/debian/{{module_name}}.service.jinja +13 -0
- pygenkit/templates/python-cli/pygenkit.yaml.jinja +19 -0
- pygenkit/templates/python-cli/pyproject.toml.jinja +53 -0
- pygenkit/templates/python-cli/src/{{module_name}}/__init__.py.jinja +4 -0
- pygenkit/templates/python-cli/src/{{module_name}}/cli.py.jinja +28 -0
- pygenkit/templates/python-cli/tests/__init__.py +0 -0
- pygenkit/templates/python-cli/tests/test_cli.py.jinja +21 -0
- pygenkit/utils/__init__.py +4 -0
- pygenkit/utils/files.py +29 -0
- pygenkit/utils/filters.py +43 -0
- pygenkit/validators/__init__.py +3 -0
- pygenkit/validators/api.py +30 -0
- pygenkit/validators/security.py +102 -0
- pygenkit/validators/version.py +77 -0
- pygenkit/validators/workflow.py +141 -0
- pygenkit-0.2.0.dist-info/METADATA +350 -0
- pygenkit-0.2.0.dist-info/RECORD +95 -0
- pygenkit-0.2.0.dist-info/WHEEL +5 -0
- pygenkit-0.2.0.dist-info/entry_points.txt +2 -0
- pygenkit-0.2.0.dist-info/licenses/LICENSE +674 -0
- pygenkit-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pygenkit.inspector import debian as debian_inspector
|
|
7
|
+
from pygenkit.inspector import detect as detect_inspector
|
|
8
|
+
from pygenkit.inspector import git as git_inspector
|
|
9
|
+
from pygenkit.inspector.pyproject import (
|
|
10
|
+
detect_project_name,
|
|
11
|
+
detect_project_version,
|
|
12
|
+
)
|
|
13
|
+
from pygenkit.models.health import CategoryScore
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def check_versioning(root: Path, pyproject_data: dict[str, Any] | None) -> CategoryScore:
|
|
17
|
+
score = CategoryScore(name="Versioning", weight=0.15)
|
|
18
|
+
pyproject_ver = None
|
|
19
|
+
|
|
20
|
+
if pyproject_data:
|
|
21
|
+
pyproject_ver = detect_project_version(pyproject_data)
|
|
22
|
+
if pyproject_ver:
|
|
23
|
+
score.passed += 1
|
|
24
|
+
score.details.append(f"pyproject.toml: {pyproject_ver}")
|
|
25
|
+
else:
|
|
26
|
+
score.issues.append("No version in pyproject.toml")
|
|
27
|
+
score.total += 1
|
|
28
|
+
|
|
29
|
+
module = _detect_module(root, pyproject_data)
|
|
30
|
+
init_ver = detect_inspector.detect_version_in_init(root, module)
|
|
31
|
+
if init_ver:
|
|
32
|
+
score.passed += 1
|
|
33
|
+
score.details.append(f"__init__.py: {init_ver}")
|
|
34
|
+
else:
|
|
35
|
+
score.issues.append("No __version__ in module __init__.py")
|
|
36
|
+
score.total += 1
|
|
37
|
+
|
|
38
|
+
debian_info = debian_inspector.inspect_debian(root)
|
|
39
|
+
if debian_info.present and debian_info.version_in_changelog:
|
|
40
|
+
score.passed += 1
|
|
41
|
+
score.details.append(f"debian/changelog: {debian_info.version_in_changelog}")
|
|
42
|
+
elif debian_info.present:
|
|
43
|
+
score.issues.append("debian/changelog has no parseable version")
|
|
44
|
+
score.total += 1
|
|
45
|
+
|
|
46
|
+
git_tags = git_inspector.detect_git_tags(root)
|
|
47
|
+
if git_tags:
|
|
48
|
+
score.passed += 1
|
|
49
|
+
score.details.append(f"{len(git_tags)} git tag(s)")
|
|
50
|
+
else:
|
|
51
|
+
score.issues.append("No git tags")
|
|
52
|
+
score.total += 1
|
|
53
|
+
|
|
54
|
+
consistent = _check_consistent(pyproject_ver, init_ver)
|
|
55
|
+
if consistent:
|
|
56
|
+
score.passed += 1
|
|
57
|
+
score.details.append("Versions are consistent")
|
|
58
|
+
else:
|
|
59
|
+
score.issues.append("Versions are inconsistent")
|
|
60
|
+
score.total += 1
|
|
61
|
+
|
|
62
|
+
score.score = score.passed / score.total if score.total > 0 else 0.0
|
|
63
|
+
return score
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def check_testing(root: Path) -> CategoryScore:
|
|
67
|
+
score = CategoryScore(name="Testing", weight=0.15)
|
|
68
|
+
|
|
69
|
+
has_tests = detect_inspector.detect_tests(root)
|
|
70
|
+
if has_tests:
|
|
71
|
+
score.passed += 1
|
|
72
|
+
score.details.append("Tests directory found")
|
|
73
|
+
else:
|
|
74
|
+
score.issues.append("No tests directory")
|
|
75
|
+
score.total += 1
|
|
76
|
+
|
|
77
|
+
if has_tests:
|
|
78
|
+
test_dir = root / "tests"
|
|
79
|
+
if test_dir.is_dir():
|
|
80
|
+
py_files = list(test_dir.rglob("test_*.py"))
|
|
81
|
+
score.details.append(f"{len(py_files)} test file(s)")
|
|
82
|
+
if py_files:
|
|
83
|
+
score.passed += 1
|
|
84
|
+
else:
|
|
85
|
+
score.issues.append("No test_*.py files in tests/")
|
|
86
|
+
score.total += 1
|
|
87
|
+
|
|
88
|
+
score.score = score.passed / score.total if score.total > 0 else 0.0
|
|
89
|
+
return score
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def check_documentation(root: Path) -> CategoryScore:
|
|
93
|
+
score = CategoryScore(name="Documentation", weight=0.15)
|
|
94
|
+
|
|
95
|
+
readme = detect_inspector.detect_readme(root)
|
|
96
|
+
if readme:
|
|
97
|
+
score.passed += 1
|
|
98
|
+
score.details.append(f"README: {readme.name}")
|
|
99
|
+
else:
|
|
100
|
+
score.issues.append("No README file")
|
|
101
|
+
score.total += 1
|
|
102
|
+
|
|
103
|
+
changelog = root / "CHANGELOG.md"
|
|
104
|
+
if changelog.exists():
|
|
105
|
+
score.passed += 1
|
|
106
|
+
score.details.append("CHANGELOG.md present")
|
|
107
|
+
else:
|
|
108
|
+
score.issues.append("No CHANGELOG.md")
|
|
109
|
+
score.total += 1
|
|
110
|
+
|
|
111
|
+
contributing = root / "CONTRIBUTING.md"
|
|
112
|
+
if contributing.exists():
|
|
113
|
+
score.passed += 1
|
|
114
|
+
score.details.append("CONTRIBUTING.md present")
|
|
115
|
+
score.total += 1
|
|
116
|
+
|
|
117
|
+
license_result = detect_inspector.detect_license(root)
|
|
118
|
+
if license_result:
|
|
119
|
+
score.passed += 1
|
|
120
|
+
score.details.append(f"License: {license_result}")
|
|
121
|
+
else:
|
|
122
|
+
score.issues.append("No license file")
|
|
123
|
+
score.total += 1
|
|
124
|
+
|
|
125
|
+
score.score = score.passed / score.total if score.total > 0 else 0.0
|
|
126
|
+
return score
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def check_cicd(root: Path) -> CategoryScore:
|
|
130
|
+
score = CategoryScore(name="CICD", weight=0.20)
|
|
131
|
+
|
|
132
|
+
wf_dir = root / ".github" / "workflows"
|
|
133
|
+
if wf_dir.is_dir():
|
|
134
|
+
wf_files = list(wf_dir.glob("*.yml"))
|
|
135
|
+
score.details.append(f"{len(wf_files)} workflow file(s)")
|
|
136
|
+
if wf_files:
|
|
137
|
+
score.passed += 1
|
|
138
|
+
score.total += 1
|
|
139
|
+
|
|
140
|
+
has_ci = any("ci" in f.name.lower() for f in wf_files)
|
|
141
|
+
has_release = any("release" in f.name.lower() for f in wf_files)
|
|
142
|
+
has_pypi = any("pypi" in f.name.lower() for f in wf_files)
|
|
143
|
+
|
|
144
|
+
if has_ci:
|
|
145
|
+
score.passed += 1
|
|
146
|
+
score.details.append("CI workflow present")
|
|
147
|
+
else:
|
|
148
|
+
score.issues.append("No CI workflow")
|
|
149
|
+
score.total += 1
|
|
150
|
+
|
|
151
|
+
if has_release:
|
|
152
|
+
score.passed += 1
|
|
153
|
+
score.details.append("Release workflow present")
|
|
154
|
+
else:
|
|
155
|
+
score.issues.append("No release workflow")
|
|
156
|
+
score.total += 1
|
|
157
|
+
|
|
158
|
+
if has_pypi:
|
|
159
|
+
score.passed += 1
|
|
160
|
+
score.details.append("PyPI publish workflow present")
|
|
161
|
+
else:
|
|
162
|
+
score.issues.append("No PyPI publish workflow")
|
|
163
|
+
score.total += 1
|
|
164
|
+
else:
|
|
165
|
+
score.issues.append("No GitHub Actions workflows")
|
|
166
|
+
score.total += 3 # ci, release, pypi
|
|
167
|
+
|
|
168
|
+
dockerfile = root / "Dockerfile"
|
|
169
|
+
if dockerfile.exists():
|
|
170
|
+
score.passed += 1
|
|
171
|
+
score.details.append("Dockerfile present")
|
|
172
|
+
score.total += 1
|
|
173
|
+
|
|
174
|
+
deploy_files = ["fly.toml", "Procfile", "railway.json", "heroku.yml"]
|
|
175
|
+
deploy_count = sum(1 for f in deploy_files if (root / f).exists())
|
|
176
|
+
if deploy_count > 0:
|
|
177
|
+
score.passed += 1
|
|
178
|
+
score.details.append(f"{deploy_count} deploy config(s) present")
|
|
179
|
+
score.total += 1
|
|
180
|
+
|
|
181
|
+
score.score = score.passed / score.total if score.total > 0 else 0.0
|
|
182
|
+
return score
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def check_security(root: Path) -> CategoryScore:
|
|
186
|
+
score = CategoryScore(name="Security", weight=0.15)
|
|
187
|
+
|
|
188
|
+
license_type = detect_inspector.detect_license(root)
|
|
189
|
+
if license_type:
|
|
190
|
+
score.passed += 1
|
|
191
|
+
score.details.append(f"License: {license_type}")
|
|
192
|
+
else:
|
|
193
|
+
score.issues.append("No license")
|
|
194
|
+
score.total += 1
|
|
195
|
+
|
|
196
|
+
git_dir = root / ".git"
|
|
197
|
+
if git_dir.is_dir():
|
|
198
|
+
score.passed += 1
|
|
199
|
+
else:
|
|
200
|
+
score.issues.append("Not a git repository")
|
|
201
|
+
score.total += 1
|
|
202
|
+
|
|
203
|
+
github_remote = git_inspector.detect_github_remote(root)
|
|
204
|
+
if github_remote:
|
|
205
|
+
score.passed += 1
|
|
206
|
+
else:
|
|
207
|
+
score.issues.append("No GitHub remote configured")
|
|
208
|
+
score.total += 1
|
|
209
|
+
|
|
210
|
+
wf_dir = root / ".github" / "workflows"
|
|
211
|
+
has_permissions = False
|
|
212
|
+
if wf_dir.is_dir():
|
|
213
|
+
for f in wf_dir.glob("*.yml"):
|
|
214
|
+
content = f.read_text(encoding="utf-8", errors="replace")
|
|
215
|
+
if "permissions:" in content:
|
|
216
|
+
has_permissions = True
|
|
217
|
+
break
|
|
218
|
+
|
|
219
|
+
if has_permissions or not wf_dir.is_dir():
|
|
220
|
+
score.passed += 1
|
|
221
|
+
else:
|
|
222
|
+
score.issues.append("Workflows missing permissions block")
|
|
223
|
+
score.total += 1
|
|
224
|
+
|
|
225
|
+
score.score = score.passed / score.total if score.total > 0 else 0.0
|
|
226
|
+
return score
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def check_packaging(root: Path, pyproject_data: dict[str, Any] | None) -> CategoryScore:
|
|
230
|
+
score = CategoryScore(name="Packaging", weight=0.10)
|
|
231
|
+
|
|
232
|
+
if pyproject_data:
|
|
233
|
+
score.passed += 1
|
|
234
|
+
score.details.append("pyproject.toml present")
|
|
235
|
+
else:
|
|
236
|
+
score.issues.append("No pyproject.toml")
|
|
237
|
+
score.total += 1
|
|
238
|
+
|
|
239
|
+
if pyproject_data:
|
|
240
|
+
pkg_data = pyproject_data.get("project", {})
|
|
241
|
+
if pkg_data.get("name"):
|
|
242
|
+
score.passed += 1
|
|
243
|
+
else:
|
|
244
|
+
score.issues.append("pyproject.toml missing project name")
|
|
245
|
+
score.total += 1
|
|
246
|
+
|
|
247
|
+
if pkg_data.get("version"):
|
|
248
|
+
score.passed += 1
|
|
249
|
+
else:
|
|
250
|
+
score.issues.append("pyproject.toml missing version")
|
|
251
|
+
score.total += 1
|
|
252
|
+
|
|
253
|
+
if pkg_data.get("description"):
|
|
254
|
+
score.passed += 1
|
|
255
|
+
else:
|
|
256
|
+
score.issues.append("pyproject.toml missing description")
|
|
257
|
+
score.total += 1
|
|
258
|
+
|
|
259
|
+
src_dir = root / "src"
|
|
260
|
+
if src_dir.is_dir():
|
|
261
|
+
score.passed += 1
|
|
262
|
+
score.details.append("src/ layout detected")
|
|
263
|
+
else:
|
|
264
|
+
score.issues.append("Not using src/ layout (recommended)")
|
|
265
|
+
score.total += 1
|
|
266
|
+
|
|
267
|
+
score.score = score.passed / score.total if score.total > 0 else 0.0
|
|
268
|
+
return score
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def check_structure(root: Path) -> CategoryScore:
|
|
272
|
+
score = CategoryScore(name="Structure", weight=0.10)
|
|
273
|
+
|
|
274
|
+
gitignore = root / ".gitignore"
|
|
275
|
+
if gitignore.exists():
|
|
276
|
+
score.passed += 1
|
|
277
|
+
else:
|
|
278
|
+
score.issues.append("No .gitignore")
|
|
279
|
+
score.total += 1
|
|
280
|
+
|
|
281
|
+
editorconfig = root / ".editorconfig"
|
|
282
|
+
if editorconfig.exists():
|
|
283
|
+
score.passed += 1
|
|
284
|
+
score.total += 1
|
|
285
|
+
|
|
286
|
+
ruff_toml = root / "ruff.toml"
|
|
287
|
+
pyproject_toml = root / "pyproject.toml"
|
|
288
|
+
ruff_configured = False
|
|
289
|
+
if ruff_toml.exists():
|
|
290
|
+
ruff_configured = True
|
|
291
|
+
elif pyproject_toml.exists():
|
|
292
|
+
data = _try_read_pyproject(pyproject_toml)
|
|
293
|
+
if data and "tool" in data and "ruff" in data["tool"]:
|
|
294
|
+
ruff_configured = True
|
|
295
|
+
if ruff_configured:
|
|
296
|
+
score.passed += 1
|
|
297
|
+
score.details.append("Ruff configured")
|
|
298
|
+
else:
|
|
299
|
+
score.issues.append("Ruff not configured")
|
|
300
|
+
score.total += 1
|
|
301
|
+
|
|
302
|
+
mypy_configured = False
|
|
303
|
+
mypy_ini = root / "mypy.ini"
|
|
304
|
+
if mypy_ini.exists():
|
|
305
|
+
mypy_configured = True
|
|
306
|
+
elif pyproject_toml.exists():
|
|
307
|
+
data = _try_read_pyproject(pyproject_toml)
|
|
308
|
+
if data and "tool" in data and "mypy" in data["tool"]:
|
|
309
|
+
mypy_configured = True
|
|
310
|
+
if mypy_configured:
|
|
311
|
+
score.passed += 1
|
|
312
|
+
score.details.append("Mypy configured")
|
|
313
|
+
else:
|
|
314
|
+
score.issues.append("Mypy not configured")
|
|
315
|
+
score.total += 1
|
|
316
|
+
|
|
317
|
+
precommit = root / ".pre-commit-config.yaml"
|
|
318
|
+
if precommit.exists():
|
|
319
|
+
score.passed += 1
|
|
320
|
+
score.details.append("pre-commit configured")
|
|
321
|
+
score.total += 1
|
|
322
|
+
|
|
323
|
+
score.score = score.passed / score.total if score.total > 0 else 0.0
|
|
324
|
+
return score
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _detect_module(root: Path, pyproject_data: dict[str, Any] | None) -> str | None:
|
|
328
|
+
name = detect_project_name(pyproject_data) if pyproject_data else None
|
|
329
|
+
return detect_inspector.detect_module(root, name)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _check_consistent(pyproject_ver: str | None, init_ver: str | None) -> bool:
|
|
333
|
+
if pyproject_ver and init_ver:
|
|
334
|
+
return pyproject_ver == init_ver
|
|
335
|
+
return bool(pyproject_ver or init_ver)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _try_read_pyproject(path: Path) -> dict[str, Any] | None:
|
|
339
|
+
try:
|
|
340
|
+
import tomllib
|
|
341
|
+
return tomllib.loads(path.read_text(encoding="utf-8"))
|
|
342
|
+
except Exception:
|
|
343
|
+
return None
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from pygenkit.inspector import debian as debian_inspector
|
|
6
|
+
from pygenkit.inspector import detect as detect_inspector
|
|
7
|
+
from pygenkit.inspector import git as git_inspector
|
|
8
|
+
from pygenkit.inspector.pyproject import (
|
|
9
|
+
detect_build_backend,
|
|
10
|
+
detect_project_name,
|
|
11
|
+
detect_project_version,
|
|
12
|
+
detect_python_requires,
|
|
13
|
+
read_pyproject,
|
|
14
|
+
)
|
|
15
|
+
from pygenkit.models.inspection import (
|
|
16
|
+
DebianInspection,
|
|
17
|
+
ProjectInspection,
|
|
18
|
+
VersionInspection,
|
|
19
|
+
WorkflowInspection,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def inspect_project(root: str | Path = ".") -> ProjectInspection:
|
|
24
|
+
root = Path(root).resolve()
|
|
25
|
+
result = ProjectInspection(root=str(root))
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
data = read_pyproject(root)
|
|
29
|
+
except FileNotFoundError:
|
|
30
|
+
result.errors.append("pyproject.toml not found")
|
|
31
|
+
return result
|
|
32
|
+
except Exception as exc:
|
|
33
|
+
result.errors.append(f"Failed to parse pyproject.toml: {exc}")
|
|
34
|
+
return result
|
|
35
|
+
|
|
36
|
+
result.name = detect_project_name(data)
|
|
37
|
+
result.version = detect_project_version(data)
|
|
38
|
+
result.python_requires = detect_python_requires(data)
|
|
39
|
+
result.build_backend = detect_build_backend(data)
|
|
40
|
+
|
|
41
|
+
if result.name:
|
|
42
|
+
result.module = detect_inspector.detect_module(root, result.name)
|
|
43
|
+
else:
|
|
44
|
+
result.module = detect_inspector.detect_module(root)
|
|
45
|
+
|
|
46
|
+
result.has_tests = detect_inspector.detect_tests(root)
|
|
47
|
+
result.license_type = detect_inspector.detect_license(root)
|
|
48
|
+
result.has_license = result.license_type is not None
|
|
49
|
+
|
|
50
|
+
init_ver = detect_inspector.detect_version_in_init(root, result.module)
|
|
51
|
+
pyproject_ver = result.version
|
|
52
|
+
debian_info = debian_inspector.inspect_debian(root)
|
|
53
|
+
result.has_debian = debian_info
|
|
54
|
+
|
|
55
|
+
github_remote = git_inspector.detect_github_remote(root)
|
|
56
|
+
result.github_remote = github_remote
|
|
57
|
+
git_tags = git_inspector.detect_git_tags(root)
|
|
58
|
+
|
|
59
|
+
workflows = _inspect_workflows(root)
|
|
60
|
+
result.has_workflows = workflows
|
|
61
|
+
|
|
62
|
+
result.versions = VersionInspection(
|
|
63
|
+
pyproject_version=pyproject_ver,
|
|
64
|
+
init_version=init_ver,
|
|
65
|
+
changelog_version=debian_info.version_in_changelog,
|
|
66
|
+
git_tags=git_tags,
|
|
67
|
+
consistent=_check_version_consistency(
|
|
68
|
+
pyproject_ver, init_ver, debian_info.version_in_changelog, git_tags
|
|
69
|
+
),
|
|
70
|
+
issues=_collect_version_issues(
|
|
71
|
+
pyproject_ver, init_ver, debian_info.version_in_changelog, git_tags
|
|
72
|
+
),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
result.warnings = _collect_warnings(result, debian_info)
|
|
76
|
+
|
|
77
|
+
return result
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _inspect_workflows(root: Path) -> WorkflowInspection:
|
|
81
|
+
wf = WorkflowInspection()
|
|
82
|
+
wf_dir = root / ".github" / "workflows"
|
|
83
|
+
if not wf_dir.is_dir():
|
|
84
|
+
return wf
|
|
85
|
+
|
|
86
|
+
for f in wf_dir.glob("*.yml"):
|
|
87
|
+
wf.files.append(f.name)
|
|
88
|
+
content = f.read_text(encoding="utf-8", errors="replace")
|
|
89
|
+
if "pypi" in content.lower() or "publish" in content.lower():
|
|
90
|
+
wf.has_pypi_publish = True
|
|
91
|
+
if "launchpad" in content.lower() or "ppa" in content.lower():
|
|
92
|
+
wf.has_launchpad = True
|
|
93
|
+
if "test" in content.lower() or "lint" in content.lower():
|
|
94
|
+
wf.has_ci = True
|
|
95
|
+
|
|
96
|
+
return wf
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _check_version_consistency(
|
|
100
|
+
pyproject_ver: str | None,
|
|
101
|
+
init_ver: str | None,
|
|
102
|
+
changelog_ver: str | None,
|
|
103
|
+
git_tags: list[str],
|
|
104
|
+
) -> bool:
|
|
105
|
+
versions = [v for v in [pyproject_ver, init_ver, changelog_ver] if v]
|
|
106
|
+
if not versions:
|
|
107
|
+
return True
|
|
108
|
+
if len(set(versions)) > 1:
|
|
109
|
+
return False
|
|
110
|
+
latest_tag = _latest_tag(git_tags)
|
|
111
|
+
if latest_tag and pyproject_ver and latest_tag != pyproject_ver:
|
|
112
|
+
return bool(not git_tags)
|
|
113
|
+
return True
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _latest_tag(tags: list[str]) -> str | None:
|
|
117
|
+
for t in tags:
|
|
118
|
+
if t.startswith("v"):
|
|
119
|
+
return t.lstrip("v")
|
|
120
|
+
return tags[0] if tags else None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _collect_version_issues(
|
|
124
|
+
pyproject_ver: str | None,
|
|
125
|
+
init_ver: str | None,
|
|
126
|
+
changelog_ver: str | None,
|
|
127
|
+
git_tags: list[str],
|
|
128
|
+
) -> list[str]:
|
|
129
|
+
issues: list[str] = []
|
|
130
|
+
if pyproject_ver and init_ver and pyproject_ver != init_ver:
|
|
131
|
+
issues.append(f"pyproject.toml version ({pyproject_ver}) != __init__.py ({init_ver})")
|
|
132
|
+
if pyproject_ver and changelog_ver and pyproject_ver != changelog_ver:
|
|
133
|
+
issues.append(
|
|
134
|
+
f"pyproject.toml ({pyproject_ver}) != debian/changelog ({changelog_ver})"
|
|
135
|
+
)
|
|
136
|
+
latest = _latest_tag(git_tags)
|
|
137
|
+
if latest and pyproject_ver and latest != pyproject_ver:
|
|
138
|
+
issues.append(f"latest git tag (v{latest}) != pyproject.toml version ({pyproject_ver})")
|
|
139
|
+
return issues
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _collect_warnings(result: ProjectInspection, debian_info: DebianInspection) -> list[str]:
|
|
143
|
+
warnings: list[str] = []
|
|
144
|
+
if not result.has_tests:
|
|
145
|
+
warnings.append("No tests directory found")
|
|
146
|
+
if not result.has_license:
|
|
147
|
+
warnings.append("No license file found")
|
|
148
|
+
if not result.build_backend:
|
|
149
|
+
warnings.append("No build backend detected in pyproject.toml")
|
|
150
|
+
if not result.github_remote:
|
|
151
|
+
warnings.append("No GitHub remote configured")
|
|
152
|
+
if result.has_debian.present and debian_info.issues:
|
|
153
|
+
warnings.extend(f"Debian: {i}" for i in debian_info.issues)
|
|
154
|
+
return warnings
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from pygenkit.models.inspection import DebianInspection
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def inspect_debian(root: str | Path) -> DebianInspection:
|
|
9
|
+
debian_dir = Path(root) / "debian"
|
|
10
|
+
result = DebianInspection(present=debian_dir.is_dir())
|
|
11
|
+
|
|
12
|
+
if not result.present:
|
|
13
|
+
return result
|
|
14
|
+
|
|
15
|
+
control = debian_dir / "control"
|
|
16
|
+
changelog = debian_dir / "changelog"
|
|
17
|
+
|
|
18
|
+
result.has_control = control.exists()
|
|
19
|
+
result.has_changelog = changelog.exists()
|
|
20
|
+
|
|
21
|
+
if result.has_changelog:
|
|
22
|
+
ver = _parse_changelog_version(changelog)
|
|
23
|
+
result.version_in_changelog = ver
|
|
24
|
+
|
|
25
|
+
issues = _check_issues(debian_dir, result)
|
|
26
|
+
result.issues = issues
|
|
27
|
+
|
|
28
|
+
return result
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _parse_changelog_version(path: Path) -> str | None:
|
|
32
|
+
try:
|
|
33
|
+
first_line = path.read_text(encoding="utf-8").split("\n")[0]
|
|
34
|
+
if "(" in first_line and ")" in first_line:
|
|
35
|
+
return first_line.split("(")[1].split(")")[0].split("-")[0]
|
|
36
|
+
except (IndexError, OSError):
|
|
37
|
+
pass
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _check_issues(debian_dir: Path, result: DebianInspection) -> list[str]:
|
|
42
|
+
issues: list[str] = []
|
|
43
|
+
if not result.has_control:
|
|
44
|
+
issues.append("debian/control is missing")
|
|
45
|
+
if not result.has_changelog:
|
|
46
|
+
issues.append("debian/changelog is missing")
|
|
47
|
+
if result.present and not (debian_dir / "rules").exists():
|
|
48
|
+
issues.append("debian/rules is missing (dh_make?)")
|
|
49
|
+
if result.present and not (debian_dir / "source" / "format").exists():
|
|
50
|
+
issues.append("debian/source/format is missing (use '3.0 (quilt)')")
|
|
51
|
+
return issues
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def detect_module(root: str | Path, project_name: str | None = None) -> str | None:
|
|
8
|
+
root = Path(root)
|
|
9
|
+
src_dir = root / "src"
|
|
10
|
+
if src_dir.is_dir():
|
|
11
|
+
candidates = sorted(
|
|
12
|
+
d for d in src_dir.iterdir()
|
|
13
|
+
if d.is_dir() and not d.name.startswith("_")
|
|
14
|
+
)
|
|
15
|
+
if candidates:
|
|
16
|
+
return candidates[0].name
|
|
17
|
+
|
|
18
|
+
top_dirs = sorted(d for d in root.iterdir() if d.is_dir() and not d.name.startswith("_"))
|
|
19
|
+
python_dirs = [d for d in top_dirs if (d / "__init__.py").exists()]
|
|
20
|
+
if python_dirs:
|
|
21
|
+
return python_dirs[0].name
|
|
22
|
+
|
|
23
|
+
if project_name:
|
|
24
|
+
candidate = project_name.replace("-", "_")
|
|
25
|
+
if (src_dir / candidate).is_dir():
|
|
26
|
+
return candidate
|
|
27
|
+
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def detect_tests(root: str | Path) -> bool:
|
|
32
|
+
root = Path(root)
|
|
33
|
+
if (root / "tests").is_dir():
|
|
34
|
+
return True
|
|
35
|
+
test_files = list(root.glob("test_*.py")) + list(root.glob("*test*.py"))
|
|
36
|
+
return len(test_files) > 0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def detect_license(root: str | Path) -> str | None:
|
|
40
|
+
root = Path(root)
|
|
41
|
+
for name in ("LICENSE", "LICENSE.txt", "LICENSE.md", "COPYING"):
|
|
42
|
+
path = root / name
|
|
43
|
+
if path.exists():
|
|
44
|
+
content = path.read_text(encoding="utf-8", errors="replace")
|
|
45
|
+
return _classify_license(content)
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _classify_license(content: str) -> str | None:
|
|
50
|
+
if "MIT" in content and "Permission is hereby granted" in content:
|
|
51
|
+
return "MIT"
|
|
52
|
+
if "Apache License" in content and "Version 2.0" in content:
|
|
53
|
+
return "Apache-2.0"
|
|
54
|
+
if "GNU GENERAL PUBLIC LICENSE" in content and "Version 3" in content:
|
|
55
|
+
return "GPL-3.0"
|
|
56
|
+
if "GNU GENERAL PUBLIC LICENSE" in content and "Version 2" in content:
|
|
57
|
+
return "GPL-2.0"
|
|
58
|
+
if "BSD" in content:
|
|
59
|
+
return "BSD"
|
|
60
|
+
if "Mozilla Public License" in content:
|
|
61
|
+
return "MPL-2.0"
|
|
62
|
+
return "custom"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def detect_version_in_init(root: str | Path, module: str | None = None) -> str | None:
|
|
66
|
+
if not module:
|
|
67
|
+
return None
|
|
68
|
+
for base in (Path(root) / "src", Path(root)):
|
|
69
|
+
init = base / module / "__init__.py"
|
|
70
|
+
if init.exists():
|
|
71
|
+
for line in init.read_text(encoding="utf-8").split("\n"):
|
|
72
|
+
m = re.match(r'__version__\s*=\s*["\']([^"\']+)["\']', line)
|
|
73
|
+
if m:
|
|
74
|
+
return m.group(1)
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def detect_readme(root: str | Path) -> Path | None:
|
|
79
|
+
root = Path(root)
|
|
80
|
+
for name in ("README.md", "README.rst", "README.txt", "README"):
|
|
81
|
+
path = root / name
|
|
82
|
+
if path.exists():
|
|
83
|
+
return path
|
|
84
|
+
return None
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def detect_github_remote(root: str | Path) -> str | None:
|
|
8
|
+
try:
|
|
9
|
+
r = subprocess.run(
|
|
10
|
+
["git", "remote", "get-url", "origin"],
|
|
11
|
+
capture_output=True, text=True, timeout=10,
|
|
12
|
+
cwd=str(root),
|
|
13
|
+
)
|
|
14
|
+
if r.returncode != 0:
|
|
15
|
+
return None
|
|
16
|
+
url = r.stdout.strip()
|
|
17
|
+
if "github.com" in url:
|
|
18
|
+
return url
|
|
19
|
+
return url
|
|
20
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def detect_git_tags(root: str | Path) -> list[str]:
|
|
25
|
+
try:
|
|
26
|
+
r = subprocess.run(
|
|
27
|
+
["git", "tag", "--sort=-version:refname"],
|
|
28
|
+
capture_output=True, text=True, timeout=10,
|
|
29
|
+
cwd=str(root),
|
|
30
|
+
)
|
|
31
|
+
if r.returncode != 0:
|
|
32
|
+
return []
|
|
33
|
+
return [t.strip() for t in r.stdout.strip().split("\n") if t.strip()]
|
|
34
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
35
|
+
return []
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def extract_github_owner_repo(url: str) -> tuple[str, str] | None:
|
|
39
|
+
if "github.com" not in url:
|
|
40
|
+
return None
|
|
41
|
+
parts = url.rstrip("/").replace(".git", "").split("/")
|
|
42
|
+
if len(parts) >= 2:
|
|
43
|
+
return (parts[-2], parts[-1])
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def is_git_repo(root: str | Path) -> bool:
|
|
48
|
+
try:
|
|
49
|
+
r = subprocess.run(
|
|
50
|
+
["git", "rev-parse", "--git-dir"],
|
|
51
|
+
capture_output=True, timeout=5, cwd=str(root),
|
|
52
|
+
)
|
|
53
|
+
return r.returncode == 0
|
|
54
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
55
|
+
return False
|