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
config/user_config.py ADDED
@@ -0,0 +1,238 @@
1
+ """
2
+ Persistent user-level configuration for ForgePy.
3
+ """
4
+
5
+ import json
6
+ import os
7
+ import tempfile
8
+ from collections.abc import Mapping
9
+ from json import JSONDecodeError
10
+ from pathlib import Path
11
+ from types import MappingProxyType
12
+
13
+
14
+ CONFIG_DIRECTORY_NAME = ".forgepy"
15
+ CONFIG_FILENAME = "config.json"
16
+
17
+ DEFAULT_CONFIG: Mapping[str, str] = MappingProxyType(
18
+ {
19
+ "default_template": "basic",
20
+ "default_location": "",
21
+ "author": "",
22
+ "license": "MIT",
23
+ }
24
+ )
25
+
26
+ SUPPORTED_SETTINGS = frozenset(DEFAULT_CONFIG)
27
+
28
+
29
+ class ForgePyConfigError(Exception):
30
+ """
31
+ Base error for ForgePy user configuration operations.
32
+ """
33
+
34
+
35
+ class ConfigFormatError(ForgePyConfigError):
36
+ """
37
+ Raised when the configuration file is not valid ForgePy JSON data.
38
+ """
39
+
40
+
41
+ class ConfigIOError(ForgePyConfigError):
42
+ """
43
+ Raised when the configuration file cannot be read or written.
44
+ """
45
+
46
+
47
+ class UnknownConfigSettingError(ForgePyConfigError):
48
+ """
49
+ Raised when a setting is not supported by ForgePy.
50
+ """
51
+
52
+
53
+ class InvalidConfigValueError(ForgePyConfigError):
54
+ """
55
+ Raised when a supported setting has an invalid value.
56
+ """
57
+
58
+
59
+ class ConfigStore:
60
+ """
61
+ Load and persist ForgePy user configuration as JSON.
62
+
63
+ A custom home directory can be provided for tests and isolated callers.
64
+ """
65
+
66
+ def __init__(
67
+ self,
68
+ home_directory: Path | None = None,
69
+ ) -> None:
70
+ self.home_directory = (
71
+ Path.home()
72
+ if home_directory is None
73
+ else Path(home_directory)
74
+ )
75
+ self.config_directory = (
76
+ self.home_directory
77
+ / CONFIG_DIRECTORY_NAME
78
+ )
79
+ self.config_path = (
80
+ self.config_directory
81
+ / CONFIG_FILENAME
82
+ )
83
+
84
+ @staticmethod
85
+ def defaults() -> dict[str, str]:
86
+ """
87
+ Return a new dictionary containing the safe defaults.
88
+ """
89
+
90
+ return dict(DEFAULT_CONFIG)
91
+
92
+ def load(self) -> dict[str, str]:
93
+ """
94
+ Load configuration or return defaults when no file exists.
95
+ """
96
+
97
+ try:
98
+ content = self.config_path.read_text(
99
+ encoding="utf-8",
100
+ )
101
+ data = json.loads(content)
102
+ except FileNotFoundError:
103
+ return self.defaults()
104
+ except UnicodeDecodeError as error:
105
+ raise ConfigFormatError(
106
+ "ForgePy configuration is not valid UTF-8 at "
107
+ f"'{self.config_path}'."
108
+ ) from error
109
+ except JSONDecodeError as error:
110
+ raise ConfigFormatError(
111
+ "ForgePy configuration contains malformed JSON "
112
+ f"at '{self.config_path}' "
113
+ f"(line {error.lineno}, column {error.colno})."
114
+ ) from error
115
+ except OSError as error:
116
+ raise ConfigIOError(
117
+ "ForgePy could not read configuration from "
118
+ f"'{self.config_path}': {error}"
119
+ ) from error
120
+
121
+ return self._normalize(data)
122
+
123
+ def save(
124
+ self,
125
+ config: Mapping[str, str],
126
+ ) -> dict[str, str]:
127
+ """
128
+ Validate and save configuration, creating its directory as needed.
129
+ """
130
+
131
+ normalized = self._normalize(config)
132
+ serialized = json.dumps(
133
+ normalized,
134
+ indent=4,
135
+ ensure_ascii=False,
136
+ ) + "\n"
137
+
138
+ try:
139
+ self.config_directory.mkdir(
140
+ parents=True,
141
+ exist_ok=True,
142
+ )
143
+
144
+ temporary_path: Path | None = None
145
+
146
+ try:
147
+ with tempfile.NamedTemporaryFile(
148
+ mode="w",
149
+ encoding="utf-8",
150
+ newline="\n",
151
+ dir=self.config_directory,
152
+ prefix=f".{CONFIG_FILENAME}.",
153
+ suffix=".tmp",
154
+ delete=False,
155
+ ) as temporary_file:
156
+ temporary_path = Path(
157
+ temporary_file.name,
158
+ )
159
+ temporary_file.write(serialized)
160
+ temporary_file.flush()
161
+ os.fsync(temporary_file.fileno())
162
+
163
+ temporary_path.replace(
164
+ self.config_path,
165
+ )
166
+ finally:
167
+ if (
168
+ temporary_path is not None
169
+ and temporary_path.exists()
170
+ ):
171
+ temporary_path.unlink()
172
+ except OSError as error:
173
+ raise ConfigIOError(
174
+ "ForgePy could not save configuration to "
175
+ f"'{self.config_path}': {error}"
176
+ ) from error
177
+
178
+ return normalized.copy()
179
+
180
+ def reset(self) -> dict[str, str]:
181
+ """
182
+ Explicitly replace persisted configuration with the safe defaults.
183
+ """
184
+
185
+ return self.save(self.defaults())
186
+
187
+ def update(
188
+ self,
189
+ setting: str,
190
+ value: str,
191
+ ) -> dict[str, str]:
192
+ """
193
+ Update one supported setting while preserving all other values.
194
+ """
195
+
196
+ self._validate_setting(setting)
197
+ self._validate_value(setting, value)
198
+
199
+ config = self.load()
200
+ config[setting] = value
201
+
202
+ return self.save(config)
203
+
204
+ def _normalize(
205
+ self,
206
+ data: object,
207
+ ) -> dict[str, str]:
208
+ if not isinstance(data, Mapping):
209
+ raise ConfigFormatError(
210
+ "ForgePy configuration must contain a JSON object."
211
+ )
212
+
213
+ normalized = self.defaults()
214
+
215
+ for setting, value in data.items():
216
+ self._validate_setting(setting)
217
+ self._validate_value(setting, value)
218
+ normalized[setting] = value
219
+
220
+ return normalized
221
+
222
+ @staticmethod
223
+ def _validate_setting(setting: object) -> None:
224
+ if setting not in SUPPORTED_SETTINGS:
225
+ raise UnknownConfigSettingError(
226
+ f"Unknown ForgePy configuration setting: '{setting}'."
227
+ )
228
+
229
+ @staticmethod
230
+ def _validate_value(
231
+ setting: object,
232
+ value: object,
233
+ ) -> None:
234
+ if not isinstance(value, str):
235
+ raise InvalidConfigValueError(
236
+ "ForgePy configuration setting "
237
+ f"'{setting}' must be a string."
238
+ )
config/version.py ADDED
@@ -0,0 +1,16 @@
1
+ """
2
+ ==================================================
3
+ ForgePy
4
+ Author : Rendy Zou
5
+ Module : Version Configuration
6
+ ==================================================
7
+
8
+ Deskripsi:
9
+ - Menyimpan sumber tunggal informasi versi aplikasi.
10
+ """
11
+
12
+ APP_NAME = "ForgePy"
13
+
14
+ VERSION = "1.0.0"
15
+
16
+ AUTHOR = "Rendy Zou"
core/__init__.py ADDED
File without changes
@@ -0,0 +1,33 @@
1
+ import subprocess
2
+ import sys
3
+ from pathlib import Path
4
+
5
+
6
+ VENV_CREATION_TIMEOUT_SECONDS = 300
7
+
8
+
9
+ class EnvironmentBuilder:
10
+ """
11
+ Membuat Virtual Environment Python.
12
+ """
13
+
14
+ def create(self, project_path: Path) -> None:
15
+
16
+ print("\n[INFO] Membuat Virtual Environment...")
17
+
18
+ try:
19
+ subprocess.run(
20
+ [
21
+ sys.executable,
22
+ "-m",
23
+ "venv",
24
+ str(project_path / ".venv")
25
+ ],
26
+ check=True,
27
+ timeout=VENV_CREATION_TIMEOUT_SECONDS,
28
+ )
29
+ except subprocess.SubprocessError as error:
30
+ print(f"[ERROR] Virtual environment creation failed: {error}")
31
+ raise
32
+
33
+ print("[OK] Virtual Environment berhasil dibuat.")
core/git_builder.py ADDED
@@ -0,0 +1,90 @@
1
+ """
2
+ ==================================================
3
+ ForgePy
4
+ Author : Rendy Zou
5
+ Module : Git Builder
6
+ ==================================================
7
+
8
+ Deskripsi:
9
+ - Menginisialisasi Git Repository.
10
+ - Membuat commit pertama.
11
+ """
12
+
13
+ import shutil
14
+ import subprocess
15
+ from pathlib import Path
16
+
17
+
18
+ GIT_INIT_TIMEOUT_SECONDS = 60
19
+ GIT_ADD_TIMEOUT_SECONDS = 120
20
+ GIT_COMMIT_TIMEOUT_SECONDS = 60
21
+
22
+
23
+ class GitBuilder:
24
+ """
25
+ Builder untuk menginisialisasi Git Repository.
26
+ """
27
+
28
+ def create(self, project_root: Path) -> None:
29
+
30
+ if shutil.which("git") is None:
31
+ raise FileNotFoundError(
32
+ "Git executable is required but was not found."
33
+ )
34
+
35
+ if (project_root / ".git").exists():
36
+ print("[INFO] Git Repository sudah ada.")
37
+ return
38
+
39
+ print("\n[INFO] Inisialisasi Git Repository...")
40
+
41
+ try:
42
+ subprocess.run(
43
+ ["git", "init"],
44
+ cwd=project_root,
45
+ check=True,
46
+ timeout=GIT_INIT_TIMEOUT_SECONDS,
47
+ )
48
+ except subprocess.SubprocessError as error:
49
+ print(f"[ERROR] Git initialization failed: {error}")
50
+ raise
51
+
52
+ try:
53
+ subprocess.run(
54
+ ["git", "add", "."],
55
+ cwd=project_root,
56
+ check=True,
57
+ timeout=GIT_ADD_TIMEOUT_SECONDS,
58
+ )
59
+ except subprocess.SubprocessError as error:
60
+ print(f"[ERROR] Git staging failed: {error}")
61
+ raise
62
+
63
+ try:
64
+
65
+ subprocess.run(
66
+ [
67
+ "git",
68
+ "commit",
69
+ "-m",
70
+ "Initial project created by ForgePy",
71
+ ],
72
+ cwd=project_root,
73
+ check=True,
74
+ timeout=GIT_COMMIT_TIMEOUT_SECONDS,
75
+ )
76
+
77
+ print("[OK] Initial Commit berhasil dibuat.")
78
+
79
+ except subprocess.CalledProcessError:
80
+
81
+ print(
82
+ "[WARNING] Initial Commit gagal.\n"
83
+ "Pastikan Git user.name dan user.email sudah dikonfigurasi."
84
+ )
85
+ raise
86
+ except subprocess.TimeoutExpired as error:
87
+ print(f"[ERROR] Initial Git commit timed out: {error}")
88
+ raise
89
+
90
+ print("[OK] Git Repository berhasil dibuat.")
@@ -0,0 +1,125 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ from builders.python_tools_builder import PythonToolsBuilder
5
+
6
+ from core.environment_builder import EnvironmentBuilder
7
+ from core.git_builder import GitBuilder
8
+ from core.requirements_installer import RequirementsInstaller
9
+ from core.vscode_builder import VSCodeBuilder
10
+
11
+ from models.project_config import ProjectConfig
12
+
13
+ from templates.template_engine.template_registry import TemplateRegistry
14
+
15
+
16
+ class ProjectPreflightError(ValueError):
17
+ """An expected project validation failure before root creation."""
18
+
19
+
20
+ class UnknownProjectTemplateError(KeyError):
21
+ """The requested project template is not registered."""
22
+
23
+
24
+ class ProjectGenerator:
25
+
26
+ def create(
27
+ self,
28
+ project_name: str,
29
+ location: str,
30
+ template_name: str = "basic",
31
+ ) -> None:
32
+
33
+ try:
34
+ location_path = Path(location).resolve()
35
+
36
+ if not location_path.exists():
37
+ raise ValueError(
38
+ f"Project location does not exist: '{location_path}'."
39
+ )
40
+
41
+ if not location_path.is_dir():
42
+ raise ValueError(
43
+ f"Project location must be a directory: '{location_path}'."
44
+ )
45
+
46
+ config = ProjectConfig(
47
+ name=project_name,
48
+ location=location_path,
49
+ )
50
+
51
+ destination = config.root
52
+
53
+ if os.path.lexists(destination):
54
+ raise FileExistsError(
55
+ f"Project destination already exists: '{destination}'."
56
+ )
57
+
58
+ resolved_destination = destination.resolve(strict=False)
59
+
60
+ if resolved_destination.parent != location_path:
61
+ raise ValueError(
62
+ "Project destination must remain directly below the "
63
+ f"selected location: '{resolved_destination}'."
64
+ )
65
+
66
+ registry = TemplateRegistry()
67
+
68
+ try:
69
+ template = registry.get(template_name)
70
+ except KeyError as error:
71
+ raise UnknownProjectTemplateError(template_name) from error
72
+
73
+ template.preflight(destination)
74
+ except ValueError as error:
75
+ raise ProjectPreflightError(str(error)) from error
76
+
77
+ destination.mkdir(
78
+ parents=True,
79
+ exist_ok=False,
80
+ )
81
+
82
+ # ==========================
83
+ # Template
84
+ # ==========================
85
+
86
+ template.create(config.root)
87
+
88
+ # ==========================
89
+ # Virtual Environment
90
+ # ==========================
91
+
92
+ EnvironmentBuilder().create(config.root)
93
+
94
+ # ==========================
95
+ # Update Python Tools
96
+ # ==========================
97
+
98
+ PythonToolsBuilder().update(config.root)
99
+
100
+ # ==========================
101
+ # Install Requirements
102
+ # ==========================
103
+
104
+ RequirementsInstaller().install(config.root)
105
+
106
+ # ==========================
107
+ # VSCode
108
+ # ==========================
109
+
110
+ VSCodeBuilder().create(
111
+ config.root,
112
+ entry_point=template.vscode_entry_point,
113
+ )
114
+
115
+ # ==========================
116
+ # Git
117
+ # ==========================
118
+
119
+ GitBuilder().create(config.root)
120
+
121
+ print()
122
+ print("=" * 40)
123
+ print("Project berhasil dibuat.")
124
+ print(config.root)
125
+ print("=" * 40)
@@ -0,0 +1,50 @@
1
+ import subprocess
2
+ from pathlib import Path
3
+
4
+
5
+ REQUIREMENTS_INSTALL_TIMEOUT_SECONDS = 900
6
+
7
+
8
+ class RequirementsInstaller:
9
+ """
10
+ Menginstal package dari requirements.txt
11
+ menggunakan virtual environment yang baru dibuat.
12
+ """
13
+
14
+ def install(self, project_path: Path) -> None:
15
+
16
+ requirements = project_path / "requirements.txt"
17
+
18
+ if not requirements.exists():
19
+ print("[WARNING] requirements.txt tidak ditemukan.")
20
+ return
21
+
22
+ # Jika requirements.txt kosong
23
+ if requirements.read_text(encoding="utf-8").strip() == "":
24
+ print("[INFO] requirements.txt kosong, tidak ada dependency yang di-install.")
25
+ return
26
+
27
+ pip = project_path / ".venv" / "Scripts" / "pip.exe"
28
+
29
+ if not pip.exists():
30
+ print("[WARNING] Virtual Environment belum tersedia.")
31
+ return
32
+
33
+ print("\n[INFO] Menginstal dependencies...")
34
+
35
+ try:
36
+ subprocess.run(
37
+ [
38
+ str(pip),
39
+ "install",
40
+ "-r",
41
+ str(requirements),
42
+ ],
43
+ check=True,
44
+ timeout=REQUIREMENTS_INSTALL_TIMEOUT_SECONDS,
45
+ )
46
+ except subprocess.SubprocessError as error:
47
+ print(f"[ERROR] Requirements installation failed: {error}")
48
+ raise
49
+
50
+ print("[OK] Dependencies berhasil di-install.")
core/vscode_builder.py ADDED
@@ -0,0 +1,103 @@
1
+ """
2
+ ==================================================
3
+ ForgePy
4
+ Author : Rendy Zou
5
+ Module : VSCode Builder
6
+ ==================================================
7
+
8
+ Deskripsi:
9
+ - Membuat konfigurasi Visual Studio Code
10
+ - (.vscode/settings.json)
11
+ - (.vscode/launch.json)
12
+ - (.vscode/tasks.json)
13
+ - (.vscode/extensions.json)
14
+ """
15
+
16
+ from pathlib import Path
17
+
18
+ from templates.vscode import (
19
+ settings_template,
20
+ launch_template,
21
+ tasks_template,
22
+ extensions_template,
23
+ )
24
+
25
+
26
+ class VSCodeBuilder:
27
+ """
28
+ Builder untuk membuat konfigurasi Visual Studio Code.
29
+ """
30
+
31
+ def create(
32
+ self,
33
+ project_root: Path,
34
+ entry_point: str | None = "app.py",
35
+ ) -> None:
36
+
37
+ vscode_folder = project_root / ".vscode"
38
+ vscode_folder.mkdir(exist_ok=True)
39
+
40
+ self._write_settings(vscode_folder)
41
+ self._write_launch(
42
+ vscode_folder,
43
+ entry_point,
44
+ )
45
+ self._write_tasks(
46
+ vscode_folder,
47
+ entry_point,
48
+ )
49
+ self._write_extensions(vscode_folder)
50
+
51
+ print("[OK] VS Code configuration berhasil dibuat.")
52
+
53
+ def _write_settings(self, vscode_folder: Path) -> None:
54
+
55
+ path = vscode_folder / "settings.json"
56
+
57
+ path.write_text(
58
+ settings_template.build(),
59
+ encoding="utf-8",
60
+ )
61
+
62
+ print(f"[OK] File dibuat : {path}")
63
+
64
+ def _write_launch(
65
+ self,
66
+ vscode_folder: Path,
67
+ entry_point: str | None,
68
+ ) -> None:
69
+
70
+ path = vscode_folder / "launch.json"
71
+
72
+ path.write_text(
73
+ launch_template.build(entry_point),
74
+ encoding="utf-8",
75
+ )
76
+
77
+ print(f"[OK] File dibuat : {path}")
78
+
79
+ def _write_tasks(
80
+ self,
81
+ vscode_folder: Path,
82
+ entry_point: str | None,
83
+ ) -> None:
84
+
85
+ path = vscode_folder / "tasks.json"
86
+
87
+ path.write_text(
88
+ tasks_template.build(entry_point),
89
+ encoding="utf-8",
90
+ )
91
+
92
+ print(f"[OK] File dibuat : {path}")
93
+
94
+ def _write_extensions(self, vscode_folder: Path) -> None:
95
+
96
+ path = vscode_folder / "extensions.json"
97
+
98
+ path.write_text(
99
+ extensions_template.build(),
100
+ encoding="utf-8",
101
+ )
102
+
103
+ print(f"[OK] File dibuat : {path}")