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
pygenkit/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ __all__ = ["__version__"]
2
+ __version__ = "0.2.0"
@@ -0,0 +1,11 @@
1
+ from pygenkit.ai.provider import LLMProvider, OpenAIProvider, create_provider
2
+ from pygenkit.ai.review import ReviewResult, get_pr_diff, review_diff
3
+
4
+ __all__ = [
5
+ "LLMProvider",
6
+ "OpenAIProvider",
7
+ "create_provider",
8
+ "ReviewResult",
9
+ "review_diff",
10
+ "get_pr_diff",
11
+ ]
pygenkit/ai/prompts.py ADDED
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ SYSTEM_PROMPT = (
4
+ "You are an expert Python code reviewer. "
5
+ "Analyze the provided diff and give concise, actionable feedback.\n\n"
6
+ "Focus on:\n"
7
+ "1. **Correctness** - bugs, logic errors, edge cases\n"
8
+ "2. **Security** - injection, hardcoded secrets, permission issues\n"
9
+ "3. **Performance** - unnecessary work, poor data structures\n"
10
+ "4. **Maintainability** - naming, complexity, duplication, error handling\n"
11
+ "5. **Style** - consistency with modern Python (3.12+) patterns\n\n"
12
+ "Output format (markdown):\n"
13
+ "## Summary\n"
14
+ "Brief 1-sentence overall assessment.\n\n"
15
+ "## Issues\n"
16
+ "| Severity | File | Line | Description |\n"
17
+ "|----------|------|------|-------------|\n"
18
+ "| high | path/file.py | 42 | Description of the issue and suggestion |\n\n"
19
+ "Severity: high, medium, low, or info.\n\n"
20
+ "## Positive\n"
21
+ "What was done well, if anything."
22
+ )
23
+
24
+
25
+ def build_user_prompt(diff: str, filename_hint: str = "") -> str:
26
+ hint = f" (focusing on {filename_hint})" if filename_hint else ""
27
+ return f"Review this Python diff{hint}:\n\n```diff\n{diff}\n```"
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import urllib.request
6
+ from abc import ABC, abstractmethod
7
+ from typing import Any
8
+
9
+
10
+ class LLMProvider(ABC):
11
+ @abstractmethod
12
+ def complete(self, system: str, user: str, **kwargs: Any) -> str:
13
+ ...
14
+
15
+
16
+ class OpenAIProvider(LLMProvider):
17
+ def __init__(self, api_key: str | None = None, model: str = "gpt-4o-mini") -> None:
18
+ self.api_key = api_key or os.environ.get("OPENAI_API_KEY", "")
19
+ if not self.api_key:
20
+ msg = "OPENAI_API_KEY is not set"
21
+ raise ValueError(msg)
22
+ self.model = model
23
+ self._base_url = "https://api.openai.com/v1/chat/completions"
24
+
25
+ def complete(self, system: str, user: str, **kwargs: Any) -> str:
26
+ body = {
27
+ "model": self.model,
28
+ "messages": [
29
+ {"role": "system", "content": system},
30
+ {"role": "user", "content": user},
31
+ ],
32
+ **kwargs,
33
+ }
34
+ req = urllib.request.Request(
35
+ self._base_url,
36
+ data=json.dumps(body).encode(),
37
+ headers={
38
+ "Authorization": f"Bearer {self.api_key}",
39
+ "Content-Type": "application/json",
40
+ },
41
+ method="POST",
42
+ )
43
+ with urllib.request.urlopen(req, timeout=60) as resp:
44
+ result: dict[str, Any] = json.loads(resp.read().decode())
45
+
46
+ choices = result.get("choices", [])
47
+ if not choices:
48
+ msg = f"OpenAI API returned no choices: {result}"
49
+ raise RuntimeError(msg)
50
+ return str(choices[0]["message"]["content"])
51
+
52
+
53
+ def create_provider(provider_name: str = "openai", **kwargs: Any) -> LLMProvider:
54
+ if provider_name == "openai":
55
+ return OpenAIProvider(**kwargs)
56
+ msg = f"Unknown provider: {provider_name}"
57
+ raise ValueError(msg)
pygenkit/ai/review.py ADDED
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from pygenkit.ai.prompts import SYSTEM_PROMPT, build_user_prompt
9
+ from pygenkit.ai.provider import LLMProvider, create_provider
10
+
11
+
12
+ @dataclass
13
+ class ReviewResult:
14
+ raw: str
15
+ summary: str = ""
16
+ issues: list[dict[str, str]] = field(default_factory=list)
17
+ positives: list[str] = field(default_factory=list)
18
+ error: str = ""
19
+
20
+
21
+ def get_pr_diff(pr_number: str, repo: str | None = None) -> str:
22
+ cmd = ["gh", "pr", "diff", pr_number]
23
+ if repo:
24
+ cmd.extend(["--repo", repo])
25
+ try:
26
+ r = subprocess.run(
27
+ cmd,
28
+ capture_output=True,
29
+ text=True,
30
+ timeout=30,
31
+ )
32
+ if r.returncode != 0:
33
+ msg = f"gh command failed: {r.stderr.strip()}"
34
+ raise RuntimeError(msg)
35
+ return r.stdout
36
+ except FileNotFoundError:
37
+ msg = "GitHub CLI (gh) is not installed"
38
+ raise RuntimeError(msg) # noqa: B904
39
+
40
+
41
+ def get_pr_diff_from_file(path: str | Path) -> str:
42
+ return Path(path).read_text(encoding="utf-8")
43
+
44
+
45
+ def review_diff(
46
+ diff: str,
47
+ provider: LLMProvider | None = None,
48
+ filename_hint: str = "",
49
+ **llm_kwargs: Any,
50
+ ) -> ReviewResult:
51
+ if provider is None:
52
+ provider = create_provider()
53
+
54
+ try:
55
+ user = build_user_prompt(diff, filename_hint)
56
+ raw = provider.complete(SYSTEM_PROMPT, user, **llm_kwargs)
57
+ return ReviewResult(raw=raw)
58
+ except Exception as exc:
59
+ return ReviewResult(raw="", error=str(exc))
@@ -0,0 +1,3 @@
1
+ from pygenkit.cli.app import cli
2
+
3
+ __all__ = ["cli"]
pygenkit/cli/app.py ADDED
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+
3
+ import typer
4
+
5
+ from pygenkit import __version__
6
+ from pygenkit.cli.commands import (
7
+ doctor_cmd,
8
+ generate_cmd,
9
+ health_cmd,
10
+ init_cmd,
11
+ inspect_cmd,
12
+ new_cmd,
13
+ release_check_cmd,
14
+ review_cmd,
15
+ validate_cmd,
16
+ )
17
+
18
+ cli = typer.Typer(
19
+ name="pygenkit",
20
+ help="Analyze Python projects and generate CI/CD pipelines for PyPI and Debian/Launchpad",
21
+ no_args_is_help=True,
22
+ )
23
+
24
+
25
+ def _version_callback(value: bool) -> None:
26
+ if value:
27
+ typer.echo(f"PyGenKit v{__version__}")
28
+ raise typer.Exit()
29
+
30
+
31
+ @cli.callback()
32
+ def _main(
33
+ version: bool = typer.Option(
34
+ False,
35
+ "--version",
36
+ "-V",
37
+ help="Show version and exit",
38
+ callback=_version_callback,
39
+ is_eager=True,
40
+ ),
41
+ ) -> None:
42
+ pass
43
+
44
+
45
+ cli.command(name="init")(init_cmd)
46
+ cli.command(name="new")(new_cmd)
47
+ cli.command(name="inspect")(inspect_cmd)
48
+ cli.command(name="validate")(validate_cmd)
49
+ cli.command(name="generate")(generate_cmd)
50
+ cli.command(name="release-check")(release_check_cmd)
51
+ cli.command(name="health")(health_cmd)
52
+ cli.command(name="review")(review_cmd)
53
+ cli.command(name="doctor")(doctor_cmd)
@@ -0,0 +1,21 @@
1
+ from pygenkit.cli.commands.doctor import doctor_cmd
2
+ from pygenkit.cli.commands.generate import generate_cmd
3
+ from pygenkit.cli.commands.health import health_cmd
4
+ from pygenkit.cli.commands.init import init_cmd
5
+ from pygenkit.cli.commands.inspect import inspect_cmd
6
+ from pygenkit.cli.commands.new import new_cmd
7
+ from pygenkit.cli.commands.release_check import release_check_cmd
8
+ from pygenkit.cli.commands.review import review_cmd
9
+ from pygenkit.cli.commands.validate import validate_cmd
10
+
11
+ __all__ = [
12
+ "init_cmd",
13
+ "inspect_cmd",
14
+ "new_cmd",
15
+ "validate_cmd",
16
+ "generate_cmd",
17
+ "release_check_cmd",
18
+ "doctor_cmd",
19
+ "health_cmd",
20
+ "review_cmd",
21
+ ]
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ import typer
8
+
9
+
10
+ def build_cmd(
11
+ clean: bool = typer.Option(True, "--clean", help="Clean before build"),
12
+ ) -> None:
13
+ project_dir = Path.cwd()
14
+ pyproject = project_dir / "pyproject.toml"
15
+ if not pyproject.exists():
16
+ typer.echo("No pyproject.toml found. Are you in a Python project?")
17
+ raise typer.Exit(code=1)
18
+
19
+ if clean:
20
+ subprocess.run(
21
+ [sys.executable, "-m", "pip", "install", "build"],
22
+ capture_output=True,
23
+ timeout=60,
24
+ )
25
+
26
+ result = subprocess.run(
27
+ [sys.executable, "-m", "build", "--sdist", "--wheel", str(project_dir)],
28
+ capture_output=True,
29
+ text=True,
30
+ timeout=120,
31
+ )
32
+
33
+ if result.returncode != 0:
34
+ typer.echo(f"Build failed:\n{result.stderr}")
35
+ raise typer.Exit(code=1)
36
+
37
+ dist_dir = project_dir / "dist"
38
+ if dist_dir.exists():
39
+ for f in sorted(dist_dir.iterdir()):
40
+ typer.echo(f" {f.name}")
41
+ typer.echo("Build complete.")
@@ -0,0 +1,119 @@
1
+ from __future__ import annotations
2
+
3
+ import shutil
4
+ import subprocess
5
+ import sys
6
+ from collections.abc import Callable
7
+ from typing import Any
8
+
9
+ import typer
10
+
11
+ from pygenkit import __version__
12
+
13
+
14
+ class DoctorCheck:
15
+ def __init__(self, name: str, check_fn: Callable[[], Any], hint: str = "") -> None:
16
+ self.name = name
17
+ self.check_fn = check_fn
18
+ self.hint = hint
19
+
20
+ def run(self) -> dict[str, Any]:
21
+ try:
22
+ result = self.check_fn()
23
+ return {"name": self.name, "ok": True, "detail": str(result), "hint": ""}
24
+ except Exception as exc:
25
+ return {"name": self.name, "ok": False, "detail": str(exc), "hint": self.hint}
26
+
27
+
28
+ def _check_python() -> str:
29
+ return sys.version.split()[0]
30
+
31
+
32
+ def _check_git() -> str:
33
+ r = subprocess.run(["git", "--version"], capture_output=True, text=True, timeout=10)
34
+ return r.stdout.strip()
35
+
36
+
37
+ def _check_gh() -> str:
38
+ if not shutil.which("gh"):
39
+ raise RuntimeError("not found")
40
+ r = subprocess.run(["gh", "--version"], capture_output=True, text=True, timeout=10)
41
+ return r.stdout.split("\n")[0] if r.stdout else "found"
42
+
43
+
44
+ def _check_gpg() -> str:
45
+ if not shutil.which("gpg"):
46
+ raise RuntimeError("not found (apt install gnupg)")
47
+ return "found"
48
+
49
+
50
+ def _check_dput() -> str:
51
+ if not shutil.which("dput"):
52
+ raise RuntimeError("not found (apt install dput)")
53
+ return "found"
54
+
55
+
56
+ def _check_debhelper() -> str:
57
+ if not shutil.which("dh"):
58
+ raise RuntimeError("not found (apt install debhelper)")
59
+ return "found"
60
+
61
+
62
+ def _check_twine() -> str:
63
+ r = subprocess.run(
64
+ [sys.executable, "-m", "twine", "--version"],
65
+ capture_output=True, text=True, timeout=10,
66
+ )
67
+ if r.returncode != 0:
68
+ raise RuntimeError("not installed (pip install twine)")
69
+ return r.stdout.strip()
70
+
71
+
72
+ def _check_build_module() -> str:
73
+ r = subprocess.run(
74
+ [sys.executable, "-m", "build", "--version"],
75
+ capture_output=True, text=True, timeout=10,
76
+ )
77
+ if r.returncode != 0:
78
+ raise RuntimeError("not installed (pip install build)")
79
+ return r.stdout.strip()
80
+
81
+
82
+ def doctor_cmd() -> None:
83
+ """Check system for required tools."""
84
+ checks = [
85
+ DoctorCheck("Python", _check_python, "Install Python 3.12+ from python.org"),
86
+ DoctorCheck("Git", _check_git, "apt install git"),
87
+ DoctorCheck("GitHub CLI", _check_gh, "Install from https://cli.github.com"),
88
+ DoctorCheck("GPG", _check_gpg, "apt install gnupg"),
89
+ DoctorCheck("dput", _check_dput, "apt install dput"),
90
+ DoctorCheck("debhelper", _check_debhelper, "apt install debhelper"),
91
+ DoctorCheck(
92
+ "twine", _check_twine, "pip install twine"
93
+ ),
94
+ DoctorCheck(
95
+ "build", _check_build_module, "pip install build"
96
+ ),
97
+ ]
98
+
99
+ typer.echo(f"PyGenKit v{__version__} — Doctor")
100
+ typer.echo("")
101
+
102
+ all_ok = True
103
+ for check in checks:
104
+ result = check.run()
105
+ if result["ok"]:
106
+ typer.echo(f" \u2713 {result['name']}: {result['detail']}")
107
+ else:
108
+ all_ok = False
109
+ detail = f" ({result['detail']})" if result["detail"] else ""
110
+ typer.echo(f" \u2717 {result['name']}: not found{detail}")
111
+ if result["hint"]:
112
+ typer.echo(f" \u2192 {result['hint']}")
113
+
114
+ typer.echo("")
115
+ if all_ok:
116
+ typer.echo("All checks passed!")
117
+ else:
118
+ typer.echo("Some checks failed. Fix the issues above and re-run.")
119
+ raise typer.Exit(code=1)
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import typer
6
+
7
+ from pygenkit.generators.orchestrator import generate_all
8
+ from pygenkit.models.config import PyGenKitConfig
9
+
10
+
11
+ def generate_cmd(
12
+
13
+ config: Path = typer.Option("pygenkit.toml", "--config", "-c", help="Config file"), # noqa: B008
14
+ dry_run: bool = typer.Option(False, "--dry-run", "-n", help="Show changes without writing"), # noqa: B008
15
+ force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing files"), # noqa: B008
16
+ ) -> None:
17
+ """Generate CI/CD pipelines, Dockerfiles, and deploy configs."""
18
+ if not config.exists():
19
+ typer.echo(f" Config not found: {config}")
20
+ typer.echo(" Run: pygenkit init <name>")
21
+ raise typer.Exit(code=1)
22
+
23
+ cfg = PyGenKitConfig.load(config)
24
+ results = generate_all(cfg, force=force)
25
+
26
+ if dry_run:
27
+ _report_dry(results)
28
+ return
29
+
30
+ total = sum(len(v) for v in results.values())
31
+ if total == 0:
32
+ typer.echo(" Nothing to generate (enable docker/deploy in config, or files already exist).") # noqa: E501
33
+ typer.echo(" Use --force to overwrite existing files.")
34
+ return
35
+
36
+ typer.echo(f" Generated {total} file(s):")
37
+ cwd = Path.cwd().resolve()
38
+ for category, paths in results.items():
39
+ for p in paths:
40
+ rel = p.resolve().relative_to(cwd)
41
+ typer.echo(f" \u2713 {category}: {rel}")
42
+
43
+ typer.echo("")
44
+ typer.echo(" Done.")
45
+
46
+
47
+ def _report_dry(results: dict[str, list[Path]]) -> None:
48
+ total = sum(len(v) for v in results.values())
49
+ typer.echo(f" Would generate {total} file(s):")
50
+ for category, paths in results.items():
51
+ for p in paths:
52
+ typer.echo(f" {category}: {p}")
@@ -0,0 +1,64 @@
1
+ from __future__ import annotations
2
+
3
+ import typer
4
+
5
+ from pygenkit.health.api import calculate_health
6
+
7
+
8
+ def _bar(pct: int, width: int = 20) -> str:
9
+ filled = int(pct / 100 * width)
10
+ empty = width - filled
11
+ return "[" + "#" * filled + "-" * empty + "]"
12
+
13
+
14
+ def health_cmd() -> None:
15
+ """Assess project health across 7 categories."""
16
+ report = calculate_health(".")
17
+
18
+ score = report.score
19
+ bar = _bar(score)
20
+ grade = _grade(score)
21
+
22
+ typer.echo(f" Health Score: {bar} {score}/100 ({grade})")
23
+ typer.echo("")
24
+
25
+ cat_keys = (
26
+ "versioning", "testing", "documentation",
27
+ "cicd", "security", "packaging", "structure",
28
+ )
29
+ for cat_key in cat_keys:
30
+ cat = report.categories.get(cat_key)
31
+ if not cat:
32
+ continue
33
+ pct = int(cat.score * 100)
34
+ cb = _bar(pct, 10)
35
+ label = cat.name.ljust(14)
36
+ typer.echo(f" {label} {cb} {pct}% ({cat.passed}/{cat.total})")
37
+
38
+ if report.issues:
39
+ typer.echo("")
40
+ typer.echo(" Issues:")
41
+ for issue in report.issues[:10]:
42
+ typer.echo(f" \u2717 {issue}")
43
+ if len(report.issues) > 10:
44
+ typer.echo(f" ... and {len(report.issues) - 10} more")
45
+
46
+ typer.echo("")
47
+ if score >= 80:
48
+ typer.echo(" Great shape!")
49
+ elif score >= 50:
50
+ typer.echo(" Room for improvement.")
51
+ else:
52
+ typer.echo(" Needs significant work.")
53
+
54
+
55
+ def _grade(score: int) -> str:
56
+ if score >= 90:
57
+ return "A"
58
+ if score >= 80:
59
+ return "B"
60
+ if score >= 65:
61
+ return "C"
62
+ if score >= 50:
63
+ return "D"
64
+ return "F"
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import typer
6
+
7
+ from pygenkit.models.config import PyGenKitConfig
8
+
9
+
10
+ def init_cmd(
11
+ name: str = typer.Argument(..., help="Project name"),
12
+ force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing config"), # noqa: B008
13
+ ) -> None:
14
+ """Create pygenkit.toml in the current directory."""
15
+ config_path = Path("pygenkit.toml").resolve()
16
+ if config_path.exists() and not force:
17
+ typer.echo(" pygenkit.toml already exists. Use --force to overwrite.")
18
+ raise typer.Exit(code=1)
19
+
20
+ content = PyGenKitConfig.generate_default(name)
21
+ config_path.write_text(content, encoding="utf-8")
22
+ typer.echo(f" Created {config_path}")
23
+ typer.echo(" Edit it with your project info, then run: pygenkit inspect")
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import typer
6
+
7
+ from pygenkit.inspector.api import inspect_project
8
+
9
+
10
+ def _fmt(val: object) -> str:
11
+ if val is None:
12
+ return "—"
13
+ return str(val)
14
+
15
+
16
+ def inspect_cmd() -> None:
17
+ """Analyze project structure, versions, and metadata."""
18
+ project_dir = Path.cwd()
19
+
20
+ typer.echo(f" Inspecting {project_dir}")
21
+ typer.echo("")
22
+
23
+ result = inspect_project(project_dir)
24
+
25
+ typer.echo(f" Project: {_fmt(result.name)}")
26
+ typer.echo(f" Version: {_fmt(result.version)}")
27
+ typer.echo(f" Module: {_fmt(result.module)}")
28
+ backend = result.build_backend
29
+ typer.echo(f" Build backend: {_fmt(backend.name if backend else None)}")
30
+ typer.echo(f" Python req: {_fmt(result.python_requires)}")
31
+ typer.echo(f" License: {_fmt(result.license_type)}")
32
+ typer.echo(f" Tests: {'yes' if result.has_tests else 'no'}")
33
+ typer.echo(f" Debian: {'yes' if result.has_debian.present else 'no'}")
34
+ typer.echo(f" GitHub: {_fmt(result.github_remote)}")
35
+ typer.echo("")
36
+
37
+ if result.versions.issues:
38
+ typer.echo(" Version issues:")
39
+ for issue in result.versions.issues:
40
+ typer.echo(f" \u2717 {issue}")
41
+
42
+ if result.warnings:
43
+ typer.echo(" Warnings:")
44
+ for w in result.warnings:
45
+ typer.echo(f" ! {w}")
46
+
47
+ if result.errors:
48
+ typer.echo(" Errors:")
49
+ for e in result.errors:
50
+ typer.echo(f" \u2717 {e}")
51
+ raise typer.Exit(code=1)
52
+
53
+ typer.echo("")
54
+ if result.versions.consistent:
55
+ typer.echo(" Versions: consistent")
56
+ else:
57
+ typer.echo(" Versions: inconsistent")
58
+ raise typer.Exit(code=1)
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import typer
6
+
7
+ from pygenkit.generators.project import ProjectGenerator
8
+
9
+
10
+ def new_cmd(
11
+ name: str = typer.Argument(..., help="Project name"),
12
+ output: Path | None = typer.Option(None, "--output", "-o", help="Output directory"), # noqa: B008
13
+ force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing directory"), # noqa: B008
14
+ ) -> None:
15
+ """Scaffold a new Python project with CI/CD, tests, and tooling ready."""
16
+ gen = ProjectGenerator()
17
+ output_dir = output or Path.cwd()
18
+ try:
19
+ root = gen.generate(
20
+ name=name,
21
+ output_dir=output_dir,
22
+ force=force,
23
+ )
24
+ except FileExistsError as exc:
25
+ typer.echo(f" Error: {exc}")
26
+ raise typer.Exit(code=1) from exc
27
+
28
+ typer.echo(f" Created {root}")
29
+ typer.echo("")
30
+ typer.echo(" Next steps:")
31
+ typer.echo(f" cd {name}")
32
+ typer.echo(" git add . && git commit -m 'Initial commit'")
33
+ typer.echo(" git remote add origin <repo-url>")
34
+ typer.echo(" git push -u origin main")
35
+ typer.echo("")
36
+ typer.echo(" Edit pygenkit.toml with your info, then run:")
37
+ typer.echo(" pygenkit generate")
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ import typer
8
+
9
+
10
+ def publish_cmd(
11
+ repository: str = typer.Option("pypi", "--repository", "-r", help="PyPI repository"), # noqa: B008
12
+ dist_dir: Path = typer.Option("dist", "--dist", help="Dist directory"), # noqa: B008
13
+ ) -> None:
14
+ dist_path = Path(dist_dir)
15
+ if not dist_path.is_dir() or not list(dist_path.glob("*.whl")):
16
+ typer.echo("No distribution files found. Run 'pygenkit build' first.")
17
+ raise typer.Exit(code=1)
18
+
19
+ result = subprocess.run(
20
+ [sys.executable, "-m", "twine", "upload", "--repository", repository, f"{dist_path}/*"],
21
+ capture_output=True,
22
+ text=True,
23
+ timeout=120,
24
+ )
25
+
26
+ if result.returncode != 0:
27
+ typer.echo(f"Publish failed:\n{result.stderr}")
28
+ raise typer.Exit(code=1)
29
+
30
+ typer.echo("Published to PyPI successfully.")