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
pygenkit/__init__.py
ADDED
pygenkit/ai/__init__.py
ADDED
|
@@ -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```"
|
pygenkit/ai/provider.py
ADDED
|
@@ -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))
|
pygenkit/cli/__init__.py
ADDED
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.")
|