forgepy-cli 1.0.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 (74) hide show
  1. builders/__init__.py +0 -0
  2. builders/base_builder.py +17 -0
  3. builders/file_builder.py +31 -0
  4. builders/folder_builder.py +30 -0
  5. builders/python_tools_builder.py +64 -0
  6. cli/__init__.py +3 -0
  7. cli/command.py +52 -0
  8. cli/commands/__init__.py +32 -0
  9. cli/commands/component_command.py +177 -0
  10. cli/commands/config_command.py +174 -0
  11. cli/commands/create_command.py +161 -0
  12. cli/commands/list_command.py +40 -0
  13. cli/commands/version_command.py +29 -0
  14. cli/dispatcher.py +61 -0
  15. cli/parser.py +84 -0
  16. components/__init__.py +1 -0
  17. components/base_component.py +30 -0
  18. components/component_context.py +21 -0
  19. components/component_installer.py +44 -0
  20. components/component_manifest.py +102 -0
  21. components/component_metadata.py +50 -0
  22. components/component_registry.py +74 -0
  23. components/component_state.py +243 -0
  24. components/component_validation.py +88 -0
  25. components/github_actions_component.py +75 -0
  26. components/pytest_component.py +40 -0
  27. components/ruff_component.py +40 -0
  28. config/__init__.py +0 -0
  29. config/default_structure.py +18 -0
  30. config/user_config.py +238 -0
  31. config/version.py +16 -0
  32. core/__init__.py +0 -0
  33. core/environment_builder.py +33 -0
  34. core/git_builder.py +90 -0
  35. core/project_generator.py +125 -0
  36. core/requirements_installer.py +50 -0
  37. core/vscode_builder.py +103 -0
  38. forgepy_cli-1.0.0.dist-info/METADATA +176 -0
  39. forgepy_cli-1.0.0.dist-info/RECORD +74 -0
  40. forgepy_cli-1.0.0.dist-info/WHEEL +5 -0
  41. forgepy_cli-1.0.0.dist-info/entry_points.txt +2 -0
  42. forgepy_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
  43. forgepy_cli-1.0.0.dist-info/top_level.txt +8 -0
  44. main.py +20 -0
  45. models/__init__.py +1 -0
  46. models/project_config.py +87 -0
  47. templates/__init__.py +0 -0
  48. templates/app_template.py +20 -0
  49. templates/basic/basic_files.py +23 -0
  50. templates/basic/basic_template.py +32 -0
  51. templates/changelog_template.py +15 -0
  52. templates/cli/cli_files.py +70 -0
  53. templates/cli/cli_template.py +55 -0
  54. templates/env_template.py +20 -0
  55. templates/gitignore_template.py +27 -0
  56. templates/library/library_files.py +28 -0
  57. templates/library/library_template.py +56 -0
  58. templates/license_template.py +21 -0
  59. templates/pyproject_template.py +15 -0
  60. templates/readme_template.py +15 -0
  61. templates/requirements_template.py +13 -0
  62. templates/template_engine/base_template.py +58 -0
  63. templates/template_engine/file_template.py +76 -0
  64. templates/template_engine/package_name.py +30 -0
  65. templates/template_engine/template_context.py +24 -0
  66. templates/template_engine/template_files.py +10 -0
  67. templates/template_engine/template_metadata.py +59 -0
  68. templates/template_engine/template_registry.py +107 -0
  69. templates/template_manager.py +58 -0
  70. templates/vscode/__init__.py +0 -0
  71. templates/vscode/extensions_template.py +32 -0
  72. templates/vscode/launch_template.py +41 -0
  73. templates/vscode/settings_template.py +37 -0
  74. templates/vscode/tasks_template.py +66 -0
@@ -0,0 +1,28 @@
1
+ """
2
+ ==================================================
3
+ ForgePy
4
+ Library Template Files
5
+ ==================================================
6
+ """
7
+
8
+ from templates.template_manager import TemplateManager
9
+
10
+
11
+ class LibraryFiles:
12
+ """Build the minimal file mapping for a Python library project."""
13
+
14
+ @staticmethod
15
+ def build(
16
+ project_name: str,
17
+ package_name: str,
18
+ ) -> dict[str, str]:
19
+ template_manager = TemplateManager()
20
+
21
+ return {
22
+ "README.md": template_manager.get_readme(project_name),
23
+ ".gitignore": template_manager.get_gitignore(),
24
+ "requirements.txt": "",
25
+ "pyproject.toml": template_manager.get_pyproject(project_name),
26
+ f"{package_name}/__init__.py": "",
27
+ "tests/__init__.py": "",
28
+ }
@@ -0,0 +1,56 @@
1
+ """
2
+ ==================================================
3
+ ForgePy
4
+ Library Template
5
+ ==================================================
6
+ """
7
+
8
+ from pathlib import Path
9
+
10
+ from templates.library.library_files import LibraryFiles
11
+ from templates.template_engine.file_template import FileTemplate
12
+ from templates.template_engine.package_name import normalize_package_name
13
+ from templates.template_engine.template_context import TemplateContext
14
+ from templates.template_engine.template_metadata import TemplateMetadata
15
+
16
+
17
+ class LibraryTemplate(FileTemplate):
18
+ """Generate a minimal reusable Python package project."""
19
+
20
+ _METADATA = TemplateMetadata(
21
+ name="library",
22
+ description="Reusable Python package template.",
23
+ version="0.1.0",
24
+ author="Rendy Zou",
25
+ tags=("python", "library", "package"),
26
+ )
27
+
28
+ _DEFAULT_VSCODE_ENTRY_POINT = None
29
+
30
+ def _build_context(
31
+ self,
32
+ project_path: Path,
33
+ ) -> TemplateContext:
34
+ return TemplateContext(
35
+ project_path=project_path,
36
+ package_name=self._normalize_package_name(project_path.name),
37
+ )
38
+
39
+ def _folders(self, context: TemplateContext) -> tuple[str, ...]:
40
+ return (
41
+ context.require_package_name(),
42
+ "tests",
43
+ )
44
+
45
+ def _files(self, context: TemplateContext) -> dict[str, str]:
46
+ return LibraryFiles.build(
47
+ project_name=context.project_name,
48
+ package_name=context.require_package_name(),
49
+ )
50
+
51
+ @staticmethod
52
+ def _normalize_package_name(project_name: str) -> str:
53
+ return normalize_package_name(
54
+ project_name,
55
+ package_label="library",
56
+ )
@@ -0,0 +1,21 @@
1
+ """
2
+ ==================================================
3
+ ForgePy
4
+ License Template
5
+ ==================================================
6
+ """
7
+
8
+
9
+ def get_license(author: str = "Your Name") -> str:
10
+ return f"""MIT License
11
+
12
+ Copyright (c) 2026 {author}
13
+
14
+ Permission is hereby granted, free of charge, to any person obtaining a copy
15
+ of this software and associated documentation files (the "Software"), to deal
16
+ in the Software without restriction, including without limitation the rights
17
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
+ copies of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
21
+ """
@@ -0,0 +1,15 @@
1
+ """
2
+ ==================================================
3
+ ForgePy
4
+ PyProject Template
5
+ ==================================================
6
+ """
7
+
8
+
9
+ def get_pyproject(project_name: str) -> str:
10
+ return f"""[project]
11
+ name = "{project_name}"
12
+ version = "0.1.0"
13
+ description = ""
14
+ requires-python = ">=3.12"
15
+ """
@@ -0,0 +1,15 @@
1
+ """
2
+ ==================================================
3
+ ForgePy
4
+ Readme Template
5
+ ==================================================
6
+ """
7
+
8
+
9
+ def get_readme(project_name: str) -> str:
10
+ return f"""# {project_name}
11
+
12
+ Generated by ForgePy.
13
+
14
+ Version: 0.1
15
+ """
@@ -0,0 +1,13 @@
1
+ """
2
+ ==================================================
3
+ ForgePy
4
+ Requirements Template
5
+ ==================================================
6
+ """
7
+
8
+
9
+ def get_requirements() -> str:
10
+ return """PySide6
11
+ pandas
12
+ openpyxl
13
+ """
@@ -0,0 +1,58 @@
1
+ """
2
+ ==================================================
3
+ ForgePy
4
+ Base Template
5
+ ==================================================
6
+ """
7
+
8
+ from abc import ABC, abstractmethod
9
+ from pathlib import Path
10
+
11
+ from templates.template_engine.template_metadata import TemplateMetadata
12
+
13
+
14
+ class BaseTemplate(ABC):
15
+ """
16
+ Seluruh template harus mewarisi class ini.
17
+ """
18
+
19
+ @property
20
+ @abstractmethod
21
+ def name(self) -> str:
22
+ """
23
+ Nama template.
24
+ """
25
+ pass
26
+
27
+ @property
28
+ def metadata(self) -> TemplateMetadata:
29
+ """Return compatibility metadata for legacy template subclasses."""
30
+
31
+ return TemplateMetadata(
32
+ name=self.name,
33
+ description="",
34
+ version="",
35
+ author="",
36
+ tags=(),
37
+ )
38
+
39
+ @property
40
+ def vscode_entry_point(self) -> str | None:
41
+ """Return the file VS Code should launch for this template."""
42
+
43
+ return "app.py"
44
+
45
+ def preflight(self, project_path: Path) -> None:
46
+ """Validate template-specific inputs before project creation."""
47
+
48
+ del project_path
49
+
50
+ @abstractmethod
51
+ def create(
52
+ self,
53
+ project_path: Path,
54
+ ) -> None:
55
+ """
56
+ Membuat project.
57
+ """
58
+ pass
@@ -0,0 +1,76 @@
1
+ """Shared execution for templates backed by folders and file mappings."""
2
+
3
+ from abc import abstractmethod
4
+ from pathlib import Path
5
+ from typing import ClassVar
6
+
7
+ from builders.file_builder import FileBuilder
8
+ from builders.folder_builder import FolderBuilder
9
+ from templates.template_engine.base_template import BaseTemplate
10
+ from templates.template_engine.template_context import TemplateContext
11
+ from templates.template_engine.template_metadata import TemplateMetadata
12
+
13
+
14
+ class FileTemplate(BaseTemplate):
15
+ """Write template-owned folders and files from focused subclass hooks."""
16
+
17
+ _METADATA: ClassVar[TemplateMetadata]
18
+ _DEFAULT_VSCODE_ENTRY_POINT: ClassVar[str | None]
19
+
20
+ def __init__(self) -> None:
21
+ self._vscode_entry_point: str | None = (
22
+ self._DEFAULT_VSCODE_ENTRY_POINT
23
+ )
24
+
25
+ @property
26
+ def metadata(self) -> TemplateMetadata:
27
+ return self._METADATA
28
+
29
+ @property
30
+ def name(self) -> str:
31
+ return self.metadata.name
32
+
33
+ @property
34
+ def vscode_entry_point(self) -> str | None:
35
+ return self._vscode_entry_point
36
+
37
+ def preflight(self, project_path: Path) -> None:
38
+ """Build context to validate template-specific inputs."""
39
+
40
+ self._build_context(project_path)
41
+
42
+ def create(self, project_path: Path) -> None:
43
+ context = self._build_context(project_path)
44
+
45
+ FolderBuilder().create(
46
+ context.project_path,
47
+ list(self._folders(context)),
48
+ )
49
+
50
+ file_builder = FileBuilder()
51
+
52
+ for filename, content in self._files(context).items():
53
+ file_builder.write(
54
+ context.project_path / filename,
55
+ content,
56
+ )
57
+
58
+ self._vscode_entry_point = self._vscode_entry_point_for(context)
59
+
60
+ def _build_context(self, project_path: Path) -> TemplateContext:
61
+ return TemplateContext(project_path=project_path)
62
+
63
+ @abstractmethod
64
+ def _folders(self, context: TemplateContext) -> tuple[str, ...]:
65
+ """Return template-owned folders in creation order."""
66
+
67
+ @abstractmethod
68
+ def _files(self, context: TemplateContext) -> dict[str, str]:
69
+ """Return template-owned file content in write order."""
70
+
71
+ def _vscode_entry_point_for(
72
+ self,
73
+ context: TemplateContext,
74
+ ) -> str | None:
75
+ del context
76
+ return self._DEFAULT_VSCODE_ENTRY_POINT
@@ -0,0 +1,30 @@
1
+ import keyword
2
+ import re
3
+
4
+
5
+ def normalize_package_name(
6
+ project_name: str,
7
+ *,
8
+ package_label: str,
9
+ ) -> str:
10
+ """Return a stable ASCII Python package name for a project."""
11
+
12
+ package_name = re.sub(
13
+ r"[^a-z0-9_]+",
14
+ "_",
15
+ project_name.lower(),
16
+ ).strip("_")
17
+
18
+ if not package_name:
19
+ raise ValueError(
20
+ "Project name must contain letters or digits for the "
21
+ f"{package_label} package."
22
+ )
23
+
24
+ if package_name[0].isdigit():
25
+ package_name = f"_{package_name}"
26
+
27
+ if keyword.iskeyword(package_name):
28
+ package_name = f"{package_name}_"
29
+
30
+ return package_name
@@ -0,0 +1,24 @@
1
+ """Per-generation data used by file-based templates."""
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+
7
+ @dataclass(frozen=True, slots=True)
8
+ class TemplateContext:
9
+ """Keep project and optional package names together during generation."""
10
+
11
+ project_path: Path
12
+ package_name: str | None = None
13
+
14
+ @property
15
+ def project_name(self) -> str:
16
+ return self.project_path.name
17
+
18
+ def require_package_name(self) -> str:
19
+ """Return the package name for package-oriented templates."""
20
+
21
+ if self.package_name is None:
22
+ raise ValueError("Template context does not define a package name.")
23
+
24
+ return self.package_name
@@ -0,0 +1,10 @@
1
+ """Compatibility facade for the original basic-template file API."""
2
+
3
+ from templates.basic.basic_files import BasicFiles
4
+
5
+
6
+ class TemplateFiles:
7
+
8
+ @staticmethod
9
+ def basic(project_name: str) -> dict[str, str]:
10
+ return BasicFiles.build(project_name)
@@ -0,0 +1,59 @@
1
+ """
2
+ ==================================================
3
+ ForgePy
4
+ Template Metadata
5
+ ==================================================
6
+ """
7
+
8
+ from dataclasses import dataclass
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class TemplateMetadata:
13
+ """Describe a registered project template."""
14
+
15
+ name: str
16
+ description: str
17
+ version: str
18
+ author: str
19
+ tags: tuple[str, ...]
20
+
21
+ def __post_init__(self) -> None:
22
+ for field_name in (
23
+ "name",
24
+ "description",
25
+ "version",
26
+ "author",
27
+ ):
28
+ if not isinstance(getattr(self, field_name), str):
29
+ raise TypeError(
30
+ f"Template metadata {field_name} must be a string."
31
+ )
32
+
33
+ if not self.name.strip():
34
+ raise ValueError(
35
+ "Template metadata name must not be empty."
36
+ )
37
+
38
+ if isinstance(self.tags, (str, bytes)):
39
+ raise TypeError(
40
+ "Template metadata tags must be an iterable of strings."
41
+ )
42
+
43
+ try:
44
+ normalized_tags = tuple(self.tags)
45
+ except TypeError as error:
46
+ raise TypeError(
47
+ "Template metadata tags must be an iterable of strings."
48
+ ) from error
49
+
50
+ if not all(isinstance(tag, str) for tag in normalized_tags):
51
+ raise TypeError(
52
+ "Template metadata tags must contain only strings."
53
+ )
54
+
55
+ object.__setattr__(
56
+ self,
57
+ "tags",
58
+ normalized_tags,
59
+ )
@@ -0,0 +1,107 @@
1
+ """
2
+ ==================================================
3
+ ForgePy
4
+ Template Registry
5
+ ==================================================
6
+ """
7
+
8
+ from dataclasses import dataclass
9
+
10
+ from templates.basic.basic_template import BasicTemplate
11
+ from templates.cli.cli_template import CliTemplate
12
+ from templates.library.library_template import LibraryTemplate
13
+ from templates.template_engine.base_template import BaseTemplate
14
+ from templates.template_engine.template_metadata import TemplateMetadata
15
+
16
+
17
+ @dataclass(frozen=True, slots=True)
18
+ class _TemplateRegistration:
19
+ template: BaseTemplate
20
+ metadata: TemplateMetadata
21
+
22
+
23
+ class TemplateRegistry:
24
+
25
+ def __init__(self) -> None:
26
+
27
+ self._registrations: dict[str, _TemplateRegistration] = {}
28
+
29
+ self.register(BasicTemplate())
30
+ self.register(LibraryTemplate())
31
+ self.register(CliTemplate())
32
+
33
+ @property
34
+ def templates(self) -> dict[str, BaseTemplate]:
35
+ """Return a compatibility snapshot of registered templates."""
36
+
37
+ return {
38
+ name: registration.template
39
+ for name, registration in self._registrations.items()
40
+ }
41
+
42
+ def register(
43
+ self,
44
+ template: BaseTemplate,
45
+ ) -> None:
46
+ """Register a template using its metadata name as the lookup key."""
47
+
48
+ if not isinstance(template, BaseTemplate):
49
+ raise TypeError("Template must inherit from BaseTemplate.")
50
+
51
+ template_name = template.name
52
+
53
+ if not isinstance(template_name, str):
54
+ raise TypeError("Template name must be a string.")
55
+
56
+ if not template_name.strip():
57
+ raise ValueError("Template name must not be empty.")
58
+
59
+ metadata = template.metadata
60
+
61
+ if not isinstance(metadata, TemplateMetadata):
62
+ raise TypeError("Template metadata must be TemplateMetadata.")
63
+
64
+ if not metadata.name.strip():
65
+ raise ValueError("Template metadata name must not be empty.")
66
+
67
+ if template_name != metadata.name:
68
+ raise ValueError(
69
+ "Template name and metadata name must match."
70
+ )
71
+
72
+ if metadata.name in self._registrations:
73
+ raise ValueError(
74
+ f"Template '{metadata.name}' is already registered."
75
+ )
76
+
77
+ self._registrations[metadata.name] = _TemplateRegistration(
78
+ template=template,
79
+ metadata=metadata,
80
+ )
81
+
82
+ def get(
83
+ self,
84
+ name: str,
85
+ ) -> BaseTemplate:
86
+
87
+ return self._registrations[name].template
88
+
89
+ def get_metadata(
90
+ self,
91
+ name: str,
92
+ ) -> TemplateMetadata:
93
+ """Return metadata for one registered template."""
94
+
95
+ return self._registrations[name].metadata
96
+
97
+ def list_templates(self) -> dict[str, BaseTemplate]:
98
+
99
+ return self.templates
100
+
101
+ def list_metadata(self) -> tuple[TemplateMetadata, ...]:
102
+ """Return registered metadata in registration order."""
103
+
104
+ return tuple(
105
+ registration.metadata
106
+ for registration in self._registrations.values()
107
+ )
@@ -0,0 +1,58 @@
1
+ """
2
+ ==================================================
3
+ ForgePy
4
+ Template Manager
5
+ ==================================================
6
+ """
7
+
8
+ from templates.app_template import get_app
9
+ from templates.gitignore_template import get_gitignore
10
+ from templates.readme_template import get_readme
11
+ from templates.requirements_template import get_requirements
12
+
13
+ from templates.license_template import get_license
14
+ from templates.changelog_template import get_changelog
15
+ from templates.env_template import (
16
+ get_env,
17
+ get_env_example,
18
+ )
19
+ from templates.pyproject_template import get_pyproject
20
+
21
+
22
+ class TemplateManager:
23
+
24
+ def get_app(
25
+ self,
26
+ project_name: str,
27
+ ) -> str:
28
+ return get_app(project_name)
29
+
30
+ def get_gitignore(self) -> str:
31
+ return get_gitignore()
32
+
33
+ def get_readme(
34
+ self,
35
+ project_name: str,
36
+ ) -> str:
37
+ return get_readme(project_name)
38
+
39
+ def get_requirements(self) -> str:
40
+ return get_requirements()
41
+
42
+ def get_license(self) -> str:
43
+ return get_license()
44
+
45
+ def get_changelog(self) -> str:
46
+ return get_changelog()
47
+
48
+ def get_env(self) -> str:
49
+ return get_env()
50
+
51
+ def get_env_example(self) -> str:
52
+ return get_env_example()
53
+
54
+ def get_pyproject(
55
+ self,
56
+ project_name: str,
57
+ ) -> str:
58
+ return get_pyproject(project_name)
File without changes
@@ -0,0 +1,32 @@
1
+ """
2
+ ==================================================
3
+ ForgePy
4
+ Author : Rendy Zou
5
+ Module : VSCode Extensions Template
6
+ ==================================================
7
+
8
+ Deskripsi:
9
+ - Merekomendasikan extension Visual Studio Code.
10
+ """
11
+
12
+ import json
13
+
14
+
15
+ def build() -> str:
16
+
17
+ extensions = {
18
+ "recommendations": [
19
+ "ms-python.python",
20
+ "ms-python.debugpy",
21
+ "ms-python.vscode-pylance",
22
+ "ms-python.black-formatter",
23
+ "charliermarsh.ruff",
24
+ "eamodio.gitlens"
25
+ ]
26
+ }
27
+
28
+ return json.dumps(
29
+ extensions,
30
+ indent=4,
31
+ ensure_ascii=False,
32
+ )
@@ -0,0 +1,41 @@
1
+ """
2
+ ==================================================
3
+ ForgePy
4
+ Author : Rendy Zou
5
+ Module : VSCode Launch Template
6
+ ==================================================
7
+
8
+ Deskripsi:
9
+ - Template launch.json untuk Visual Studio Code.
10
+ - Menambahkan konfigurasi F5 hanya jika template memiliki entry point.
11
+ """
12
+
13
+ import json
14
+
15
+
16
+ def build(entry_point: str | None = "app.py") -> str:
17
+
18
+ configurations = []
19
+
20
+ if entry_point is not None:
21
+ configurations.append(
22
+ {
23
+ "name": f"Python: {entry_point}",
24
+ "type": "debugpy",
25
+ "request": "launch",
26
+ "program": f"${{workspaceFolder}}/{entry_point}",
27
+ "console": "integratedTerminal",
28
+ "justMyCode": True,
29
+ }
30
+ )
31
+
32
+ launch = {
33
+ "version": "0.2.0",
34
+ "configurations": configurations,
35
+ }
36
+
37
+ return json.dumps(
38
+ launch,
39
+ indent=4,
40
+ ensure_ascii=False,
41
+ )