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,33 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import typer
6
+
7
+ from pygenkit.config.loader import ConfigLoader
8
+ from pygenkit.services.release import ReleaseService
9
+
10
+
11
+ def release_cmd(
12
+ config: Path = typer.Option("pygenkit.yaml", "--config", "-c", help="Config file"), # noqa: B008
13
+ dry_run: bool = typer.Option(False, "--dry-run", "-n", help="Print actions without executing"), # noqa: B008
14
+ bump: str = typer.Option("patch", "--bump", help="Version bump: patch, minor, major"), # noqa: B008
15
+ ) -> None:
16
+ config_path = Path(config)
17
+ if not config_path.exists():
18
+ typer.echo(f"Config file not found: {config_path}")
19
+ raise typer.Exit(code=1)
20
+
21
+ proj_config = ConfigLoader.load(config_path)
22
+ service = ReleaseService(proj_config)
23
+
24
+ if dry_run:
25
+ service.dry_run(bump)
26
+ else:
27
+ result = service.run(bump)
28
+ typer.echo(f"Tag created: {result['tag']}")
29
+ typer.echo(f"Version: {result['version']}")
30
+ if result.get("pushed"):
31
+ typer.echo("Changes pushed to remote.")
32
+ msg = "GitHub Actions will handle PyPI publish, GitHub Release, and Launchpad upload."
33
+ typer.echo(msg)
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import typer
6
+
7
+
8
+ def release_check_cmd(
9
+ config: Path = typer.Option("pygenkit.toml", "--config", "-c", help="Config file"), # noqa: B008
10
+ ) -> None:
11
+ """Verify that the project is ready for release."""
12
+ if not config.exists():
13
+ typer.echo(f" Config not found: {config}")
14
+ typer.echo(" Run: pygenkit init <name>")
15
+ raise typer.Exit(code=1)
16
+
17
+ typer.echo(" [release-check] Readiness check coming in Stage 5.")
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import typer
6
+
7
+ from pygenkit.ai.review import get_pr_diff, get_pr_diff_from_file, review_diff
8
+
9
+
10
+ def review_cmd(
11
+ pr: str = typer.Argument("", help="PR number (e.g. 42) or path to diff file"), # noqa: B008
12
+ repo: str = typer.Option("", "--repo", help="GitHub repo (owner/repo)"), # noqa: B008
13
+ model: str = typer.Option("gpt-4o-mini", "--model", "-m", help="LLM model"), # noqa: B008
14
+ dry_run: bool = typer.Option(False, "--dry-run", "-n", help="Show diff without reviewing"), # noqa: B008
15
+ ) -> None:
16
+ """Review a GitHub PR diff using AI."""
17
+ diff: str = ""
18
+
19
+ if not pr:
20
+ typer.echo(" Error: PR number or path to diff file required")
21
+ raise typer.Exit(code=1)
22
+
23
+ diff_path = Path(pr)
24
+ if diff_path.exists():
25
+ diff = get_pr_diff_from_file(diff_path)
26
+ source = pr
27
+ else:
28
+ try:
29
+ diff = get_pr_diff(pr, repo or None)
30
+ source = f"PR #{pr}" + (f" ({repo})" if repo else "")
31
+ except RuntimeError as exc:
32
+ typer.echo(f" Error: {exc}")
33
+ raise typer.Exit(code=1) from exc
34
+
35
+ if not diff.strip():
36
+ typer.echo(" Empty diff — nothing to review.")
37
+ return
38
+
39
+ if dry_run:
40
+ typer.echo(f" Diff from {source}:")
41
+ typer.echo("")
42
+ typer.echo(diff[:2000] + ("..." if len(diff) > 2000 else ""))
43
+ return
44
+
45
+ typer.echo(f" Reviewing {source}...")
46
+ typer.echo("")
47
+
48
+ try:
49
+ result = review_diff(diff, filename_hint=source, model=model)
50
+ except ValueError as exc:
51
+ typer.echo(f" Error: {exc}")
52
+ raise typer.Exit(code=1) from exc
53
+
54
+ if result.error:
55
+ typer.echo(f" Review failed: {result.error}")
56
+ raise typer.Exit(code=1)
57
+
58
+ typer.echo(result.raw)
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ import typer
4
+
5
+ from pygenkit.validators.api import has_errors, validate_project
6
+
7
+
8
+ def validate_cmd() -> None:
9
+ """Check version consistency, workflow quality, and security."""
10
+ project_dir = "."
11
+
12
+ results = validate_project(project_dir)
13
+
14
+ if not results:
15
+ typer.echo(" No issues found.")
16
+ return
17
+
18
+ categories = {"version": "Version", "workflow": "Workflow", "security": "Security"}
19
+ for cat_key, cat_label in categories.items():
20
+ cat_issues = [r["message"] for r in results if r["category"] == cat_key]
21
+ if cat_issues:
22
+ typer.echo(f" {cat_label}:")
23
+ for msg in cat_issues:
24
+ typer.echo(f" \u2717 {msg}")
25
+
26
+ if has_errors(results):
27
+ typer.echo("")
28
+ typer.echo(" Validation failed.")
29
+ raise typer.Exit(code=1)
pygenkit/cli.py ADDED
@@ -0,0 +1,4 @@
1
+ from pygenkit.cli.app import cli
2
+
3
+ if __name__ == "__main__":
4
+ cli()
@@ -0,0 +1,3 @@
1
+ from pygenkit.models.config import PyGenKitConfig
2
+
3
+ __all__ = ["PyGenKitConfig"]
@@ -0,0 +1,13 @@
1
+ from pygenkit.generators.deploy import DeployGenerator
2
+ from pygenkit.generators.docker import DockerGenerator
3
+ from pygenkit.generators.github_actions import GitHubActionsGenerator
4
+ from pygenkit.generators.orchestrator import generate_all
5
+ from pygenkit.generators.project import ProjectGenerator
6
+
7
+ __all__ = [
8
+ "GitHubActionsGenerator",
9
+ "DockerGenerator",
10
+ "DeployGenerator",
11
+ "ProjectGenerator",
12
+ "generate_all",
13
+ ]
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from pygenkit.render.engine import RenderEngine
7
+
8
+
9
+ class BaseGenerator:
10
+ def __init__(self, subdir: str) -> None:
11
+ templates_dir = Path(__file__).resolve().parent.parent / "templates" / subdir
12
+ self._engine = RenderEngine(templates_dir)
13
+
14
+ def render(
15
+ self,
16
+ template_name: str,
17
+ output_dir: str | Path,
18
+ output_name: str,
19
+ context: dict[str, Any],
20
+ force: bool = False,
21
+ ) -> Path | None:
22
+ out = Path(output_dir) / output_name
23
+ if out.exists() and not force:
24
+ return None
25
+ return self._engine.render_to_file(template_name, out, context)
26
+
27
+ def list_templates(self) -> list[str]:
28
+ d = Path(self._engine.templates_dir)
29
+ if not d.is_dir():
30
+ return []
31
+ return sorted(f.name for f in d.glob("**/*.j2"))
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from pygenkit.generators.base import BaseGenerator
7
+ from pygenkit.models.config import PyGenKitConfig
8
+
9
+
10
+ class DeployGenerator(BaseGenerator):
11
+ def __init__(self) -> None:
12
+ super().__init__("deploy")
13
+
14
+ def generate(
15
+ self,
16
+ config: PyGenKitConfig,
17
+ output_dir: str | Path,
18
+ force: bool = False,
19
+ ) -> list[Path]:
20
+ generated: list[Path] = []
21
+ ctx = self._build_context(config)
22
+
23
+ targets = {
24
+ "fly.toml": ("fly.toml.j2", config.deploy.fly),
25
+ "Procfile": ("Procfile.j2", config.deploy.heroku),
26
+ "railway.json": ("railway.json.j2", config.deploy.railway),
27
+ }
28
+
29
+ for out_name, (template, enabled) in targets.items():
30
+ if not enabled:
31
+ continue
32
+ out = self.render(template, output_dir, out_name, ctx, force=force)
33
+ if out is not None:
34
+ generated.append(out)
35
+
36
+ return generated
37
+
38
+ def _build_context(self, config: PyGenKitConfig) -> dict[str, Any]:
39
+ return {
40
+ "project": {"name": config.project.name},
41
+ "deploy": {
42
+ "primary_region": config.deploy.primary_region,
43
+ "port": config.deploy.port,
44
+ "memory": config.deploy.memory,
45
+ "cpu_kind": config.deploy.cpu_kind,
46
+ "cpus": config.deploy.cpus,
47
+ },
48
+ }
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from pygenkit.generators.base import BaseGenerator
7
+ from pygenkit.models.config import PyGenKitConfig
8
+
9
+
10
+ class DockerGenerator(BaseGenerator):
11
+ def __init__(self) -> None:
12
+ super().__init__("docker")
13
+
14
+ def generate(
15
+ self,
16
+ config: PyGenKitConfig,
17
+ output_dir: str | Path,
18
+ force: bool = False,
19
+ ) -> list[Path]:
20
+ generated: list[Path] = []
21
+ ctx = self._build_context(config)
22
+
23
+ out = self.render("Dockerfile.j2", output_dir, "Dockerfile", ctx, force=force)
24
+ if out is not None:
25
+ generated.append(out)
26
+
27
+ out = self.render(
28
+ "docker-compose.yml.j2", output_dir, "docker-compose.yml", ctx, force=force
29
+ )
30
+ if out is not None:
31
+ generated.append(out)
32
+
33
+ return generated
34
+
35
+ def _build_context(self, config: PyGenKitConfig) -> dict[str, Any]:
36
+ return {
37
+ "project": {"name": config.project.name},
38
+ "docker": {
39
+ "base_image": config.docker.base_image,
40
+ "port": config.docker.port,
41
+ "volumes": config.docker.volumes,
42
+ },
43
+ }
@@ -0,0 +1,68 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from pygenkit.generators.base import BaseGenerator
7
+ from pygenkit.models.config import PyGenKitConfig
8
+
9
+
10
+ class GitHubActionsGenerator(BaseGenerator):
11
+ def __init__(self) -> None:
12
+ super().__init__("github/workflows")
13
+
14
+ def generate(
15
+ self,
16
+ config: PyGenKitConfig,
17
+ output_dir: str | Path,
18
+ force: bool = False,
19
+ ) -> list[Path]:
20
+ generated: list[Path] = []
21
+ ctx = self._build_context(config)
22
+ wf_dir = Path(output_dir) / ".github" / "workflows"
23
+ wf_dir.mkdir(parents=True, exist_ok=True)
24
+
25
+ workflows = {
26
+ "ci.yml": ("ci.yml.j2", config.github.ci),
27
+ "release.yml": ("release.yml.j2", config.github.release),
28
+ "publish-pypi.yml": ("publish-pypi.yml.j2", config.github.publish_pypi),
29
+ "publish-launchpad.yml": (
30
+ "publish-launchpad.yml.j2",
31
+ config.github.publish_launchpad,
32
+ ),
33
+ }
34
+
35
+ for out_name, (template, enabled) in workflows.items():
36
+ if not enabled:
37
+ continue
38
+ out = self.render(template, wf_dir, out_name, ctx, force=force)
39
+ if out is not None:
40
+ generated.append(out)
41
+
42
+ return generated
43
+
44
+ def _build_context(self, config: PyGenKitConfig) -> dict[str, Any]:
45
+ return {
46
+ "project": {
47
+ "name": config.project.name,
48
+ "extras": config.project.extras,
49
+ },
50
+ "ci": {
51
+ "python_versions": config.ci.python_versions,
52
+ "runner": config.ci.runner,
53
+ "lint": config.ci.lint,
54
+ "type_check": config.ci.type_check,
55
+ },
56
+ "release": {
57
+ "branch": config.release.branch,
58
+ "tag_prefix": config.release.tag_prefix,
59
+ },
60
+ "pypi": {
61
+ "environment": config.pypi.environment,
62
+ "trusted_publishing": config.pypi.trusted_publishing,
63
+ },
64
+ "debian": {
65
+ "owner": config.debian.owner,
66
+ "ppa": config.debian.ppa,
67
+ },
68
+ }
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from pygenkit.generators.deploy import DeployGenerator
6
+ from pygenkit.generators.docker import DockerGenerator
7
+ from pygenkit.generators.github_actions import GitHubActionsGenerator
8
+ from pygenkit.models.config import PyGenKitConfig
9
+
10
+
11
+ def generate_all(
12
+ config: PyGenKitConfig,
13
+ output_dir: str | Path = ".",
14
+ force: bool = False,
15
+ ) -> dict[str, list[Path]]:
16
+ results: dict[str, list[Path]] = {}
17
+
18
+ gh = GitHubActionsGenerator()
19
+ results["github"] = gh.generate(config, output_dir, force=force)
20
+
21
+ if config.docker.enabled:
22
+ dkr = DockerGenerator()
23
+ results["docker"] = dkr.generate(config, output_dir, force=force)
24
+ else:
25
+ results["docker"] = []
26
+
27
+ if config.deploy.enabled:
28
+ dep = DeployGenerator()
29
+ results["deploy"] = dep.generate(config, output_dir, force=force)
30
+ else:
31
+ results["deploy"] = []
32
+
33
+ return results
@@ -0,0 +1,110 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ import subprocess
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from pygenkit.generators.base import BaseGenerator
10
+ from pygenkit.models.config import PyGenKitConfig
11
+ from pygenkit.utils.filters import TemplateFilters
12
+
13
+
14
+ class ProjectGenerator(BaseGenerator):
15
+ def __init__(self) -> None:
16
+ super().__init__("project")
17
+
18
+ def generate(
19
+ self,
20
+ name: str,
21
+ output_dir: str | Path = ".",
22
+ force: bool = False,
23
+ author_name: str = "",
24
+ author_email: str = "",
25
+ github_owner: str = "",
26
+ description: str = "",
27
+ version: str = "0.1.0",
28
+ ) -> Path:
29
+ root = Path(output_dir).resolve() / name
30
+ if root.exists() and not force:
31
+ msg = f"{root} already exists. Use --force to overwrite."
32
+ raise FileExistsError(msg)
33
+
34
+ module_name = TemplateFilters.snake_case(name)
35
+ author_name = author_name or _git_config("user.name") or "Your Name"
36
+ author_email = author_email or _git_config("user.email") or "your@email.com"
37
+ github_owner = github_owner or _git_config("github.user") or "your-username"
38
+ description = description or f"{name} — generated by PyGenKit"
39
+ year = datetime.now().year
40
+
41
+ ctx: dict[str, Any] = {
42
+ "project": {
43
+ "name": name,
44
+ "version": version,
45
+ "description": description,
46
+ },
47
+ "module_name": module_name,
48
+ "author": {
49
+ "name": author_name,
50
+ "email": author_email,
51
+ },
52
+ "github": {
53
+ "owner": github_owner,
54
+ },
55
+ "year": year,
56
+ }
57
+
58
+ files: list[tuple[str, str]] = [
59
+ ("src/__init__.py", f"src/{module_name}/__init__.py"),
60
+ ("src/cli.py", f"src/{module_name}/cli.py"),
61
+ ("tests/__init__.py", "tests/__init__.py"),
62
+ ("tests/test_cli.py", "tests/test_cli.py"),
63
+ ("pyproject.toml", "pyproject.toml"),
64
+ ("README.md", "README.md"),
65
+ ("LICENSE", "LICENSE"),
66
+ (".gitignore", ".gitignore"),
67
+ (".editorconfig", ".editorconfig"),
68
+ (".pre-commit-config.yaml", ".pre-commit-config.yaml"),
69
+ ]
70
+
71
+ for template_rel, output_rel in files:
72
+ out = root / output_rel
73
+ out.parent.mkdir(parents=True, exist_ok=True)
74
+ self.render(template_rel + ".j2", root, output_rel, ctx, force=True)
75
+
76
+ _generate_pygenkit_toml(root, name, github_owner)
77
+
78
+ _run_commands(root)
79
+
80
+ return root
81
+
82
+
83
+ def _git_config(key: str) -> str:
84
+ try:
85
+ r = subprocess.run(
86
+ ["git", "config", "--global", key],
87
+ capture_output=True, text=True, timeout=5,
88
+ )
89
+ return r.stdout.strip() if r.returncode == 0 else ""
90
+ except (subprocess.SubprocessError, FileNotFoundError):
91
+ return ""
92
+
93
+
94
+ def _generate_pygenkit_toml(root: Path, name: str, github_owner: str) -> None:
95
+ content = PyGenKitConfig.generate_default(name)
96
+ toml_path = root / "pygenkit.toml"
97
+ toml_path.write_text(content, encoding="utf-8")
98
+
99
+ if github_owner and github_owner != "your-username":
100
+ raw = toml_path.read_text(encoding="utf-8")
101
+ raw = raw.replace('owner = ""', f'owner = "{github_owner}"')
102
+ toml_path.write_text(raw, encoding="utf-8")
103
+
104
+
105
+ def _run_commands(root: Path) -> None:
106
+ with contextlib.suppress(subprocess.SubprocessError, FileNotFoundError):
107
+ subprocess.run(
108
+ ["git", "init"],
109
+ cwd=str(root), capture_output=True, timeout=10,
110
+ )
@@ -0,0 +1,3 @@
1
+ from pygenkit.health.api import calculate_health
2
+
3
+ __all__ = ["calculate_health"]
pygenkit/health/api.py ADDED
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from pygenkit.health.checks import (
7
+ check_cicd,
8
+ check_documentation,
9
+ check_packaging,
10
+ check_security,
11
+ check_structure,
12
+ check_testing,
13
+ check_versioning,
14
+ )
15
+ from pygenkit.inspector.pyproject import read_pyproject
16
+ from pygenkit.models.health import HealthReport
17
+
18
+
19
+ def calculate_health(root: str | Path = ".") -> HealthReport:
20
+ root_path = Path(root).resolve()
21
+ pyproject_data = _safe_read_pyproject(root_path)
22
+
23
+ checks = [
24
+ check_versioning(root_path, pyproject_data),
25
+ check_testing(root_path),
26
+ check_documentation(root_path),
27
+ check_cicd(root_path),
28
+ check_security(root_path),
29
+ check_packaging(root_path, pyproject_data),
30
+ check_structure(root_path),
31
+ ]
32
+
33
+ categories = {c.name.lower(): c for c in checks}
34
+ weighted_sum = sum(c.score * c.weight for c in checks)
35
+ total_weight = sum(c.weight for c in checks)
36
+ final_score = int(round(weighted_sum / total_weight * 100)) if total_weight > 0 else 0
37
+
38
+ all_issues: list[str] = []
39
+ all_suggestions: list[str] = []
40
+ for c in checks:
41
+ all_issues.extend(c.issues)
42
+ for detail in c.details:
43
+ all_suggestions.append(f"{c.name}: {detail}")
44
+
45
+ return HealthReport(
46
+ score=final_score,
47
+ categories=categories,
48
+ issues=all_issues,
49
+ suggestions=all_suggestions,
50
+ )
51
+
52
+
53
+ def _safe_read_pyproject(root: Path) -> dict[str, Any] | None:
54
+ try:
55
+ return read_pyproject(root)
56
+ except (FileNotFoundError, Exception):
57
+ return None