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.
Files changed (95) hide show
  1. pygenkit/__init__.py +2 -0
  2. pygenkit/ai/__init__.py +11 -0
  3. pygenkit/ai/prompts.py +27 -0
  4. pygenkit/ai/provider.py +57 -0
  5. pygenkit/ai/review.py +59 -0
  6. pygenkit/cli/__init__.py +3 -0
  7. pygenkit/cli/app.py +53 -0
  8. pygenkit/cli/commands/__init__.py +21 -0
  9. pygenkit/cli/commands/build.py +41 -0
  10. pygenkit/cli/commands/doctor.py +119 -0
  11. pygenkit/cli/commands/generate.py +52 -0
  12. pygenkit/cli/commands/health.py +64 -0
  13. pygenkit/cli/commands/init.py +23 -0
  14. pygenkit/cli/commands/inspect.py +58 -0
  15. pygenkit/cli/commands/new.py +37 -0
  16. pygenkit/cli/commands/publish.py +30 -0
  17. pygenkit/cli/commands/release.py +33 -0
  18. pygenkit/cli/commands/release_check.py +17 -0
  19. pygenkit/cli/commands/review.py +58 -0
  20. pygenkit/cli/commands/validate.py +29 -0
  21. pygenkit/cli.py +4 -0
  22. pygenkit/config/__init__.py +3 -0
  23. pygenkit/generators/__init__.py +13 -0
  24. pygenkit/generators/base.py +31 -0
  25. pygenkit/generators/deploy.py +48 -0
  26. pygenkit/generators/docker.py +43 -0
  27. pygenkit/generators/github_actions.py +68 -0
  28. pygenkit/generators/orchestrator.py +33 -0
  29. pygenkit/generators/project.py +110 -0
  30. pygenkit/health/__init__.py +3 -0
  31. pygenkit/health/api.py +57 -0
  32. pygenkit/health/checks.py +343 -0
  33. pygenkit/inspector/__init__.py +3 -0
  34. pygenkit/inspector/api.py +154 -0
  35. pygenkit/inspector/debian.py +51 -0
  36. pygenkit/inspector/detect.py +84 -0
  37. pygenkit/inspector/git.py +55 -0
  38. pygenkit/inspector/pyproject.py +74 -0
  39. pygenkit/models/__init__.py +25 -0
  40. pygenkit/models/config.py +266 -0
  41. pygenkit/models/health.py +22 -0
  42. pygenkit/models/inspection.py +56 -0
  43. pygenkit/render/__init__.py +3 -0
  44. pygenkit/render/engine.py +41 -0
  45. pygenkit/services/__init__.py +0 -0
  46. pygenkit/templates/deploy/Procfile.j2 +1 -0
  47. pygenkit/templates/deploy/fly.toml.j2 +14 -0
  48. pygenkit/templates/deploy/railway.json.j2 +12 -0
  49. pygenkit/templates/docker/Dockerfile.j2 +15 -0
  50. pygenkit/templates/docker/docker-compose.yml.j2 +17 -0
  51. pygenkit/templates/github/workflows/ci.yml.j2 +33 -0
  52. pygenkit/templates/github/workflows/publish-launchpad.yml.j2 +37 -0
  53. pygenkit/templates/github/workflows/publish-pypi.yml.j2 +32 -0
  54. pygenkit/templates/github/workflows/release.yml.j2 +28 -0
  55. pygenkit/templates/project/LICENSE.j2 +21 -0
  56. pygenkit/templates/project/README.md.j2 +21 -0
  57. pygenkit/templates/project/pyproject.toml.j2 +51 -0
  58. pygenkit/templates/project/src/__init__.py.j2 +2 -0
  59. pygenkit/templates/project/src/cli.py.j2 +5 -0
  60. pygenkit/templates/project/tests/__init__.py.j2 +0 -0
  61. pygenkit/templates/project/tests/test_cli.py.j2 +5 -0
  62. pygenkit/templates/python-cli/CHANGELOG.md.jinja +7 -0
  63. pygenkit/templates/python-cli/LICENSE.jinja +204 -0
  64. pygenkit/templates/python-cli/Makefile.jinja +31 -0
  65. pygenkit/templates/python-cli/README.md.jinja +86 -0
  66. pygenkit/templates/python-cli/debian/changelog.jinja +5 -0
  67. pygenkit/templates/python-cli/debian/control.jinja +23 -0
  68. pygenkit/templates/python-cli/debian/copyright.jinja +23 -0
  69. pygenkit/templates/python-cli/debian/install.jinja +1 -0
  70. pygenkit/templates/python-cli/debian/links.jinja +1 -0
  71. pygenkit/templates/python-cli/debian/postinst.jinja +15 -0
  72. pygenkit/templates/python-cli/debian/prerm.jinja +14 -0
  73. pygenkit/templates/python-cli/debian/rules.jinja +11 -0
  74. pygenkit/templates/python-cli/debian/source/options.jinja +2 -0
  75. pygenkit/templates/python-cli/debian/{{module_name}}.service.jinja +13 -0
  76. pygenkit/templates/python-cli/pygenkit.yaml.jinja +19 -0
  77. pygenkit/templates/python-cli/pyproject.toml.jinja +53 -0
  78. pygenkit/templates/python-cli/src/{{module_name}}/__init__.py.jinja +4 -0
  79. pygenkit/templates/python-cli/src/{{module_name}}/cli.py.jinja +28 -0
  80. pygenkit/templates/python-cli/tests/__init__.py +0 -0
  81. pygenkit/templates/python-cli/tests/test_cli.py.jinja +21 -0
  82. pygenkit/utils/__init__.py +4 -0
  83. pygenkit/utils/files.py +29 -0
  84. pygenkit/utils/filters.py +43 -0
  85. pygenkit/validators/__init__.py +3 -0
  86. pygenkit/validators/api.py +30 -0
  87. pygenkit/validators/security.py +102 -0
  88. pygenkit/validators/version.py +77 -0
  89. pygenkit/validators/workflow.py +141 -0
  90. pygenkit-0.2.0.dist-info/METADATA +350 -0
  91. pygenkit-0.2.0.dist-info/RECORD +95 -0
  92. pygenkit-0.2.0.dist-info/WHEEL +5 -0
  93. pygenkit-0.2.0.dist-info/entry_points.txt +2 -0
  94. pygenkit-0.2.0.dist-info/licenses/LICENSE +674 -0
  95. 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,3 @@
1
+ from pygenkit.inspector.api import inspect_project
2
+
3
+ __all__ = ["inspect_project"]
@@ -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