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.
- builders/__init__.py +0 -0
- builders/base_builder.py +17 -0
- builders/file_builder.py +31 -0
- builders/folder_builder.py +30 -0
- builders/python_tools_builder.py +64 -0
- cli/__init__.py +3 -0
- cli/command.py +52 -0
- cli/commands/__init__.py +32 -0
- cli/commands/component_command.py +177 -0
- cli/commands/config_command.py +174 -0
- cli/commands/create_command.py +161 -0
- cli/commands/list_command.py +40 -0
- cli/commands/version_command.py +29 -0
- cli/dispatcher.py +61 -0
- cli/parser.py +84 -0
- components/__init__.py +1 -0
- components/base_component.py +30 -0
- components/component_context.py +21 -0
- components/component_installer.py +44 -0
- components/component_manifest.py +102 -0
- components/component_metadata.py +50 -0
- components/component_registry.py +74 -0
- components/component_state.py +243 -0
- components/component_validation.py +88 -0
- components/github_actions_component.py +75 -0
- components/pytest_component.py +40 -0
- components/ruff_component.py +40 -0
- config/__init__.py +0 -0
- config/default_structure.py +18 -0
- config/user_config.py +238 -0
- config/version.py +16 -0
- core/__init__.py +0 -0
- core/environment_builder.py +33 -0
- core/git_builder.py +90 -0
- core/project_generator.py +125 -0
- core/requirements_installer.py +50 -0
- core/vscode_builder.py +103 -0
- forgepy_cli-1.0.0.dist-info/METADATA +176 -0
- forgepy_cli-1.0.0.dist-info/RECORD +74 -0
- forgepy_cli-1.0.0.dist-info/WHEEL +5 -0
- forgepy_cli-1.0.0.dist-info/entry_points.txt +2 -0
- forgepy_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
- forgepy_cli-1.0.0.dist-info/top_level.txt +8 -0
- main.py +20 -0
- models/__init__.py +1 -0
- models/project_config.py +87 -0
- templates/__init__.py +0 -0
- templates/app_template.py +20 -0
- templates/basic/basic_files.py +23 -0
- templates/basic/basic_template.py +32 -0
- templates/changelog_template.py +15 -0
- templates/cli/cli_files.py +70 -0
- templates/cli/cli_template.py +55 -0
- templates/env_template.py +20 -0
- templates/gitignore_template.py +27 -0
- templates/library/library_files.py +28 -0
- templates/library/library_template.py +56 -0
- templates/license_template.py +21 -0
- templates/pyproject_template.py +15 -0
- templates/readme_template.py +15 -0
- templates/requirements_template.py +13 -0
- templates/template_engine/base_template.py +58 -0
- templates/template_engine/file_template.py +76 -0
- templates/template_engine/package_name.py +30 -0
- templates/template_engine/template_context.py +24 -0
- templates/template_engine/template_files.py +10 -0
- templates/template_engine/template_metadata.py +59 -0
- templates/template_engine/template_registry.py +107 -0
- templates/template_manager.py +58 -0
- templates/vscode/__init__.py +0 -0
- templates/vscode/extensions_template.py +32 -0
- templates/vscode/launch_template.py +41 -0
- templates/vscode/settings_template.py +37 -0
- 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}")
|