pygenkit 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pygenkit/__init__.py +2 -0
- pygenkit/ai/__init__.py +11 -0
- pygenkit/ai/prompts.py +27 -0
- pygenkit/ai/provider.py +57 -0
- pygenkit/ai/review.py +59 -0
- pygenkit/cli/__init__.py +3 -0
- pygenkit/cli/app.py +53 -0
- pygenkit/cli/commands/__init__.py +21 -0
- pygenkit/cli/commands/build.py +41 -0
- pygenkit/cli/commands/doctor.py +119 -0
- pygenkit/cli/commands/generate.py +52 -0
- pygenkit/cli/commands/health.py +64 -0
- pygenkit/cli/commands/init.py +23 -0
- pygenkit/cli/commands/inspect.py +58 -0
- pygenkit/cli/commands/new.py +37 -0
- pygenkit/cli/commands/publish.py +30 -0
- pygenkit/cli/commands/release.py +33 -0
- pygenkit/cli/commands/release_check.py +17 -0
- pygenkit/cli/commands/review.py +58 -0
- pygenkit/cli/commands/validate.py +29 -0
- pygenkit/cli.py +4 -0
- pygenkit/config/__init__.py +3 -0
- pygenkit/generators/__init__.py +13 -0
- pygenkit/generators/base.py +31 -0
- pygenkit/generators/deploy.py +48 -0
- pygenkit/generators/docker.py +43 -0
- pygenkit/generators/github_actions.py +68 -0
- pygenkit/generators/orchestrator.py +33 -0
- pygenkit/generators/project.py +110 -0
- pygenkit/health/__init__.py +3 -0
- pygenkit/health/api.py +57 -0
- pygenkit/health/checks.py +343 -0
- pygenkit/inspector/__init__.py +3 -0
- pygenkit/inspector/api.py +154 -0
- pygenkit/inspector/debian.py +51 -0
- pygenkit/inspector/detect.py +84 -0
- pygenkit/inspector/git.py +55 -0
- pygenkit/inspector/pyproject.py +74 -0
- pygenkit/models/__init__.py +25 -0
- pygenkit/models/config.py +266 -0
- pygenkit/models/health.py +22 -0
- pygenkit/models/inspection.py +56 -0
- pygenkit/render/__init__.py +3 -0
- pygenkit/render/engine.py +41 -0
- pygenkit/services/__init__.py +0 -0
- pygenkit/templates/deploy/Procfile.j2 +1 -0
- pygenkit/templates/deploy/fly.toml.j2 +14 -0
- pygenkit/templates/deploy/railway.json.j2 +12 -0
- pygenkit/templates/docker/Dockerfile.j2 +15 -0
- pygenkit/templates/docker/docker-compose.yml.j2 +17 -0
- pygenkit/templates/github/workflows/ci.yml.j2 +33 -0
- pygenkit/templates/github/workflows/publish-launchpad.yml.j2 +37 -0
- pygenkit/templates/github/workflows/publish-pypi.yml.j2 +32 -0
- pygenkit/templates/github/workflows/release.yml.j2 +28 -0
- pygenkit/templates/project/LICENSE.j2 +21 -0
- pygenkit/templates/project/README.md.j2 +21 -0
- pygenkit/templates/project/pyproject.toml.j2 +51 -0
- pygenkit/templates/project/src/__init__.py.j2 +2 -0
- pygenkit/templates/project/src/cli.py.j2 +5 -0
- pygenkit/templates/project/tests/__init__.py.j2 +0 -0
- pygenkit/templates/project/tests/test_cli.py.j2 +5 -0
- pygenkit/templates/python-cli/CHANGELOG.md.jinja +7 -0
- pygenkit/templates/python-cli/LICENSE.jinja +204 -0
- pygenkit/templates/python-cli/Makefile.jinja +31 -0
- pygenkit/templates/python-cli/README.md.jinja +86 -0
- pygenkit/templates/python-cli/debian/changelog.jinja +5 -0
- pygenkit/templates/python-cli/debian/control.jinja +23 -0
- pygenkit/templates/python-cli/debian/copyright.jinja +23 -0
- pygenkit/templates/python-cli/debian/install.jinja +1 -0
- pygenkit/templates/python-cli/debian/links.jinja +1 -0
- pygenkit/templates/python-cli/debian/postinst.jinja +15 -0
- pygenkit/templates/python-cli/debian/prerm.jinja +14 -0
- pygenkit/templates/python-cli/debian/rules.jinja +11 -0
- pygenkit/templates/python-cli/debian/source/options.jinja +2 -0
- pygenkit/templates/python-cli/debian/{{module_name}}.service.jinja +13 -0
- pygenkit/templates/python-cli/pygenkit.yaml.jinja +19 -0
- pygenkit/templates/python-cli/pyproject.toml.jinja +53 -0
- pygenkit/templates/python-cli/src/{{module_name}}/__init__.py.jinja +4 -0
- pygenkit/templates/python-cli/src/{{module_name}}/cli.py.jinja +28 -0
- pygenkit/templates/python-cli/tests/__init__.py +0 -0
- pygenkit/templates/python-cli/tests/test_cli.py.jinja +21 -0
- pygenkit/utils/__init__.py +4 -0
- pygenkit/utils/files.py +29 -0
- pygenkit/utils/filters.py +43 -0
- pygenkit/validators/__init__.py +3 -0
- pygenkit/validators/api.py +30 -0
- pygenkit/validators/security.py +102 -0
- pygenkit/validators/version.py +77 -0
- pygenkit/validators/workflow.py +141 -0
- pygenkit-0.2.0.dist-info/METADATA +350 -0
- pygenkit-0.2.0.dist-info/RECORD +95 -0
- pygenkit-0.2.0.dist-info/WHEEL +5 -0
- pygenkit-0.2.0.dist-info/entry_points.txt +2 -0
- pygenkit-0.2.0.dist-info/licenses/LICENSE +674 -0
- pygenkit-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,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,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
|
+
)
|
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
|