djangx 1.5.5__tar.gz → 1.5.6__tar.gz
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.
- {djangx-1.5.5 → djangx-1.5.6}/PKG-INFO +2 -1
- {djangx-1.5.5 → djangx-1.5.6}/pyproject.toml +2 -1
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/__init__.py +14 -6
- djangx-1.5.5/src/djangx/management/commands/generators/file.py → djangx-1.5.6/src/djangx/management/commands/generate.py +59 -6
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/management/commands/startproject.py +370 -61
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/management/settings/apps.py +2 -1
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/management/urls.py +5 -1
- djangx-1.5.5/src/djangx/management/commands/generate.py +0 -61
- djangx-1.5.5/src/djangx/management/commands/generators/__init__.py +0 -1
- {djangx-1.5.5 → djangx-1.5.6}/LICENSE +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/README.md +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/api/__init__.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/api/backends/__init__.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/api/backends/auth.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/api/backends/server/__init__.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/api/backends/server/asgi.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/api/backends/server/wsgi.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/api/backends/storages.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/api/settings/__init__.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/api/settings/auth.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/api/settings/databases.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/api/settings/server.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/api/settings/storages.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/api/types/__init__.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/api/types/auth.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/api/types/databases.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/api/types/storages.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/api/urls.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/management/__init__.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/management/cli.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/management/commands/__init__.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/management/commands/collectstatic.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/management/commands/helpers/__init__.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/management/commands/helpers/art.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/management/commands/helpers/run.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/management/commands/runbuild.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/management/commands/runinstall.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/management/commands/runserver.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/management/commands/tailwind.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/management/settings/__init__.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/management/settings/runcommands.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/management/settings/security.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/management/settings/tailwind.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/management/types/__init__.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/management/types/apps.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/py.typed +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/settings.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/types.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/ui/__init__.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/ui/admin.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/ui/settings/__init__.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/ui/settings/contactinfo.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/ui/settings/org.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/ui/settings/social.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/ui/static/ui/css/.gitignore +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/ui/static/ui/css/aos.css +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/ui/static/ui/css/bootstrap-icons.min.css +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/ui/static/ui/css/fonts/bootstrap-icons.woff +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/ui/static/ui/css/fonts/bootstrap-icons.woff2 +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/ui/static/ui/img/apple-touch-icon.png +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/ui/static/ui/img/favicon.ico +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/ui/static/ui/img/logo.png +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/ui/static/ui/js/aos-init.js +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/ui/static/ui/js/aos.js +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/ui/static/ui/js/preloader.js +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/ui/static/ui/js/scroll-top.js +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/ui/templates/ui/footer.html +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/ui/templates/ui/header.html +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/ui/templates/ui/hero.html +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/ui/templates/ui/index.html +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/ui/templates/ui/preloader.html +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/ui/templates/ui/scroll-top.html +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/ui/templatetags/__init__.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/ui/templatetags/contactinfo.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/ui/templatetags/org.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/ui/templatetags/social.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/ui/templatetags/tailwind_css.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/ui/types/__init__.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/ui/types/contactinfo.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/ui/types/org.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/ui/types/social.py +0 -0
- {djangx-1.5.5 → djangx-1.5.6}/src/djangx/ui/urls.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: djangx
|
|
3
|
-
Version: 1.5.
|
|
3
|
+
Version: 1.5.6
|
|
4
4
|
Summary: Build and deploy Django apps with confidence.
|
|
5
5
|
Author: Kevin Wasike Wakhisi
|
|
6
6
|
Author-email: Kevin Wasike Wakhisi <kevin@christianwhocodes.space>
|
|
@@ -34,6 +34,7 @@ Requires-Dist: django-phonenumber-field[phonenumberslite]>=8.4.0
|
|
|
34
34
|
Requires-Dist: django-watchfiles>=1.4.0
|
|
35
35
|
Requires-Dist: pyperclip>=1.11.0
|
|
36
36
|
Requires-Dist: python-dotenv>=1.2.1
|
|
37
|
+
Requires-Dist: rich>=14.3.2
|
|
37
38
|
Requires-Python: >=3.12
|
|
38
39
|
Project-URL: homepage, https://github.com/christianwhocodes/djangx#readme
|
|
39
40
|
Project-URL: repository, https://github.com/christianwhocodes/djangx
|
|
@@ -13,7 +13,7 @@ djx = "djangx.management.cli:main"
|
|
|
13
13
|
|
|
14
14
|
[project]
|
|
15
15
|
name = "djangx"
|
|
16
|
-
version = "1.5.
|
|
16
|
+
version = "1.5.6"
|
|
17
17
|
description = "Build and deploy Django apps with confidence."
|
|
18
18
|
readme = "README.md"
|
|
19
19
|
license = { file = "LICENSE" }
|
|
@@ -31,6 +31,7 @@ dependencies = [
|
|
|
31
31
|
"django-watchfiles>=1.4.0",
|
|
32
32
|
"pyperclip>=1.11.0",
|
|
33
33
|
"python-dotenv>=1.2.1",
|
|
34
|
+
"rich>=14.3.2",
|
|
34
35
|
]
|
|
35
36
|
|
|
36
37
|
[dependency-groups]
|
|
@@ -43,6 +43,10 @@ PROJECT_INIT_NAME: str = PROJECT_DIR.name
|
|
|
43
43
|
|
|
44
44
|
PROJECT_MAIN_APP_NAME: str = "home"
|
|
45
45
|
|
|
46
|
+
# bools
|
|
47
|
+
|
|
48
|
+
INCLUDE_PROJECT_MAIN_APP: bool = PROJECT_MAIN_APP_DIR.exists() and PROJECT_MAIN_APP_DIR.is_dir()
|
|
49
|
+
|
|
46
50
|
# Settings configuration classes
|
|
47
51
|
|
|
48
52
|
_ValueType: TypeAlias = Optional[str | bool | list[str] | pathlib.Path | int]
|
|
@@ -206,14 +210,18 @@ class Conf:
|
|
|
206
210
|
|
|
207
211
|
except (FileNotFoundError, KeyError, ValueError) as e:
|
|
208
212
|
cls._validated = False
|
|
213
|
+
print(f"Not in a valid {PKG_DISPLAY_NAME} project directory.", Text.ERROR)
|
|
209
214
|
print(
|
|
210
|
-
f"
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
215
|
+
f"A valid project requires: pyproject.toml with a 'tool.{PKG_NAME}' section (even if empty)",
|
|
216
|
+
Text.INFO,
|
|
217
|
+
)
|
|
218
|
+
print(
|
|
219
|
+
[
|
|
220
|
+
("Create a new project: ", None),
|
|
221
|
+
(f"uvx {PKG_NAME} startproject (if uv is installed.)", Text.HIGHLIGHT),
|
|
222
|
+
]
|
|
216
223
|
)
|
|
224
|
+
print(f"Validation error: {e}")
|
|
217
225
|
|
|
218
226
|
except Exception as e:
|
|
219
227
|
cls._validated = False
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import builtins
|
|
2
2
|
import pathlib
|
|
3
|
+
from enum import StrEnum
|
|
3
4
|
from typing import Any, Optional, cast
|
|
4
5
|
|
|
5
|
-
from christianwhocodes.generators import
|
|
6
|
+
from christianwhocodes.generators import (
|
|
7
|
+
FileGenerator,
|
|
8
|
+
FileGeneratorOption,
|
|
9
|
+
PgPassFileGenerator,
|
|
10
|
+
PgServiceFileGenerator,
|
|
11
|
+
SSHConfigFileGenerator,
|
|
12
|
+
)
|
|
13
|
+
from django.core.management.base import BaseCommand, CommandParser
|
|
6
14
|
|
|
7
|
-
from
|
|
15
|
+
from ... import PKG_DISPLAY_NAME, PKG_NAME, PROJECT_API_DIR, PROJECT_DIR, Conf
|
|
8
16
|
|
|
9
17
|
|
|
10
|
-
class
|
|
18
|
+
class _ServerFileGenerator(FileGenerator):
|
|
11
19
|
f"""
|
|
12
20
|
Generator for ASGI / WSGI configuration in api/server.py file.
|
|
13
21
|
|
|
@@ -27,7 +35,7 @@ class ServerFileGenerator(FileGenerator):
|
|
|
27
35
|
return f"from {PKG_NAME}.api.backends.server import application\n\napp = application\n"
|
|
28
36
|
|
|
29
37
|
|
|
30
|
-
class
|
|
38
|
+
class _VercelFileGenerator(FileGenerator):
|
|
31
39
|
"""
|
|
32
40
|
Generator for Vercel configuration file (vercel.json).
|
|
33
41
|
|
|
@@ -59,7 +67,7 @@ class VercelFileGenerator(FileGenerator):
|
|
|
59
67
|
return "\n".join(lines) + "\n"
|
|
60
68
|
|
|
61
69
|
|
|
62
|
-
class
|
|
70
|
+
class _EnvFileGenerator(FileGenerator):
|
|
63
71
|
"""
|
|
64
72
|
Generator for environment configuration file (.env.example).
|
|
65
73
|
|
|
@@ -214,4 +222,49 @@ class EnvFileGenerator(FileGenerator):
|
|
|
214
222
|
return str(value)
|
|
215
223
|
|
|
216
224
|
|
|
217
|
-
|
|
225
|
+
class FileOption(StrEnum):
|
|
226
|
+
PG_SERVICE = FileGeneratorOption.PG_SERVICE.value
|
|
227
|
+
PGPASS = FileGeneratorOption.PGPASS.value
|
|
228
|
+
SSH_CONFIG = FileGeneratorOption.SSH_CONFIG.value
|
|
229
|
+
ENV = "env"
|
|
230
|
+
SERVER = "server"
|
|
231
|
+
VERCEL = "vercel"
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class Command(BaseCommand):
|
|
235
|
+
help: str = "Generate configuration files (e.g., .env.example, vercel.json, asgi.py, wsgi.py, .pg_service.conf, pgpass.conf / .pgpass, ssh config)."
|
|
236
|
+
|
|
237
|
+
def add_arguments(self, parser: CommandParser) -> None:
|
|
238
|
+
parser.add_argument(
|
|
239
|
+
"-f",
|
|
240
|
+
"--file",
|
|
241
|
+
dest="file",
|
|
242
|
+
choices=[opt.value for opt in FileOption],
|
|
243
|
+
type=FileOption,
|
|
244
|
+
required=True,
|
|
245
|
+
help=f"Specify which file to generate (options: {', '.join(o.value for o in FileOption)}).",
|
|
246
|
+
)
|
|
247
|
+
parser.add_argument(
|
|
248
|
+
"-y",
|
|
249
|
+
"--force",
|
|
250
|
+
dest="force",
|
|
251
|
+
action="store_true",
|
|
252
|
+
help="Force overwrite without confirmation.",
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
def handle(self, *args: Any, **options: Any) -> None:
|
|
256
|
+
file_option: FileOption = FileOption(options["file"])
|
|
257
|
+
force: bool = options["force"]
|
|
258
|
+
|
|
259
|
+
generators: dict[FileOption, type[FileGenerator]] = {
|
|
260
|
+
FileOption.VERCEL: _VercelFileGenerator,
|
|
261
|
+
FileOption.SERVER: _ServerFileGenerator,
|
|
262
|
+
FileOption.PG_SERVICE: PgServiceFileGenerator,
|
|
263
|
+
FileOption.PGPASS: PgPassFileGenerator,
|
|
264
|
+
FileOption.SSH_CONFIG: SSHConfigFileGenerator,
|
|
265
|
+
FileOption.ENV: _EnvFileGenerator,
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
generator_class: type[FileGenerator] = generators[file_option]
|
|
269
|
+
generator = generator_class()
|
|
270
|
+
generator.create(force=force)
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
2
|
from enum import StrEnum
|
|
3
3
|
from pathlib import Path
|
|
4
|
+
from typing import Final
|
|
4
5
|
|
|
5
6
|
from christianwhocodes.core import ExitCode, Version
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
9
|
+
from rich.prompt import Confirm, Prompt
|
|
6
10
|
|
|
7
11
|
from ... import PKG_DISPLAY_NAME, PKG_NAME, PROJECT_DIR, PROJECT_INIT_NAME
|
|
8
12
|
|
|
@@ -12,6 +16,18 @@ __all__ = ["initialize"]
|
|
|
12
16
|
# Configuration & Templates
|
|
13
17
|
# ============================================================================
|
|
14
18
|
|
|
19
|
+
# VCS and common items that shouldn't prevent initialization
|
|
20
|
+
SAFE_DIRECTORY_ITEMS: Final[set[str]] = {
|
|
21
|
+
".git",
|
|
22
|
+
".gitignore",
|
|
23
|
+
".gitattributes",
|
|
24
|
+
".hg",
|
|
25
|
+
".hgignore",
|
|
26
|
+
"LICENSE",
|
|
27
|
+
"LICENSE.txt",
|
|
28
|
+
"LICENSE.md",
|
|
29
|
+
}
|
|
30
|
+
|
|
15
31
|
|
|
16
32
|
class PresetType(StrEnum):
|
|
17
33
|
"""Available project presets."""
|
|
@@ -94,7 +110,7 @@ A new project built with {PKG_DISPLAY_NAME}.
|
|
|
94
110
|
## Getting Started
|
|
95
111
|
|
|
96
112
|
1. Install dependencies: `uv sync`
|
|
97
|
-
2. Run development server: `
|
|
113
|
+
2. Run development server: `djx runserver`
|
|
98
114
|
""".strip()
|
|
99
115
|
|
|
100
116
|
@staticmethod
|
|
@@ -168,6 +184,32 @@ class HomeView(TemplateView):
|
|
|
168
184
|
template_name = "home/index.html"
|
|
169
185
|
"""
|
|
170
186
|
|
|
187
|
+
@staticmethod
|
|
188
|
+
def home_index_html() -> str:
|
|
189
|
+
"""Generate home app index.html content."""
|
|
190
|
+
return """{% extends "ui/index.html" %}
|
|
191
|
+
|
|
192
|
+
{% load org %}
|
|
193
|
+
|
|
194
|
+
{% block title %}
|
|
195
|
+
<title>Welcome - {% org "name" %} App</title>
|
|
196
|
+
{% endblock title %}
|
|
197
|
+
|
|
198
|
+
{% block fonts %}
|
|
199
|
+
<link href="https://fonts.googleapis.com" rel="preconnect" />
|
|
200
|
+
<link href="https://fonts.gstatic.com" rel="preconnect" crossorigin />
|
|
201
|
+
<link href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&family=Raleway:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Mulish:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"
|
|
202
|
+
rel="stylesheet" />
|
|
203
|
+
{% endblock fonts %}
|
|
204
|
+
|
|
205
|
+
{% block main %}
|
|
206
|
+
<main>
|
|
207
|
+
<section class="container-full py-8">
|
|
208
|
+
</section>
|
|
209
|
+
</main>
|
|
210
|
+
{% endblock main %}
|
|
211
|
+
"""
|
|
212
|
+
|
|
171
213
|
@staticmethod
|
|
172
214
|
def tailwind_css() -> str:
|
|
173
215
|
"""Generate Tailwind CSS content."""
|
|
@@ -289,79 +331,161 @@ class HomeView(TemplateView):
|
|
|
289
331
|
|
|
290
332
|
|
|
291
333
|
# ============================================================================
|
|
292
|
-
# File
|
|
334
|
+
# File Management
|
|
293
335
|
# ============================================================================
|
|
294
336
|
|
|
295
337
|
|
|
338
|
+
class _FileTracker:
|
|
339
|
+
"""Tracks files and directories created during initialization for rollback."""
|
|
340
|
+
|
|
341
|
+
def __init__(self):
|
|
342
|
+
"""Initialize the file tracker."""
|
|
343
|
+
self._created_paths: list[Path] = []
|
|
344
|
+
|
|
345
|
+
def track(self, path: Path) -> None:
|
|
346
|
+
"""Track a created file or directory.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
path: The path that was created.
|
|
350
|
+
"""
|
|
351
|
+
if path not in self._created_paths:
|
|
352
|
+
self._created_paths.append(path)
|
|
353
|
+
|
|
354
|
+
def cleanup_all(self) -> None:
|
|
355
|
+
"""Remove all tracked files and directories in reverse order of creation."""
|
|
356
|
+
from shutil import rmtree
|
|
357
|
+
|
|
358
|
+
# Reverse order to remove files before their parent directories
|
|
359
|
+
for path in reversed(self._created_paths):
|
|
360
|
+
try:
|
|
361
|
+
if path.exists():
|
|
362
|
+
if path.is_dir():
|
|
363
|
+
rmtree(path)
|
|
364
|
+
else:
|
|
365
|
+
path.unlink()
|
|
366
|
+
except Exception:
|
|
367
|
+
# Best effort cleanup - don't raise on cleanup failures
|
|
368
|
+
pass
|
|
369
|
+
|
|
370
|
+
self._created_paths.clear()
|
|
371
|
+
|
|
372
|
+
|
|
296
373
|
class _ProjectFileWriter:
|
|
297
374
|
"""Handles file writing operations for project initialization."""
|
|
298
375
|
|
|
299
|
-
def __init__(self, project_dir: Path):
|
|
376
|
+
def __init__(self, project_dir: Path, console: Console, tracker: _FileTracker):
|
|
300
377
|
"""Initialize the file writer.
|
|
301
378
|
|
|
302
379
|
Args:
|
|
303
380
|
project_dir: The project directory path.
|
|
381
|
+
console: Rich console for output.
|
|
382
|
+
tracker: File tracker for rollback support.
|
|
304
383
|
"""
|
|
305
384
|
self.project_dir = project_dir
|
|
385
|
+
self.console = console
|
|
386
|
+
self.tracker = tracker
|
|
306
387
|
|
|
307
|
-
def
|
|
308
|
-
"""Write content to a file.
|
|
388
|
+
def write_if_not_exists(self, filename: str, content: str) -> bool:
|
|
389
|
+
"""Write content to a file only if it doesn't exist.
|
|
309
390
|
|
|
310
391
|
Args:
|
|
311
392
|
filename: Name of the file to create.
|
|
312
393
|
content: Content to write.
|
|
313
394
|
|
|
395
|
+
Returns:
|
|
396
|
+
True if file was created, False if it already existed.
|
|
397
|
+
|
|
314
398
|
Raises:
|
|
315
399
|
IOError: If file cannot be written.
|
|
316
400
|
PermissionError: If lacking permission to write.
|
|
317
401
|
"""
|
|
318
402
|
file_path = self.project_dir / filename
|
|
403
|
+
|
|
404
|
+
if file_path.exists():
|
|
405
|
+
return False
|
|
406
|
+
|
|
319
407
|
file_path.write_text(content.strip(), encoding="utf-8")
|
|
408
|
+
self.tracker.track(file_path)
|
|
409
|
+
return True
|
|
320
410
|
|
|
321
|
-
def
|
|
322
|
-
"""Write content to a specific path.
|
|
411
|
+
def write_to_path_if_not_exists(self, path: Path, content: str) -> bool:
|
|
412
|
+
"""Write content to a specific path only if it doesn't exist.
|
|
323
413
|
|
|
324
414
|
Args:
|
|
325
415
|
path: Full path to the file.
|
|
326
416
|
content: Content to write.
|
|
327
417
|
|
|
418
|
+
Returns:
|
|
419
|
+
True if file was created, False if it already existed.
|
|
420
|
+
|
|
328
421
|
Raises:
|
|
329
422
|
IOError: If file cannot be written.
|
|
330
423
|
PermissionError: If lacking permission to write.
|
|
331
424
|
"""
|
|
425
|
+
if path.exists():
|
|
426
|
+
return False
|
|
427
|
+
|
|
332
428
|
path.write_text(content.strip(), encoding="utf-8")
|
|
429
|
+
self.tracker.track(path)
|
|
430
|
+
return True
|
|
333
431
|
|
|
334
|
-
def ensure_dir(self, path: Path) ->
|
|
432
|
+
def ensure_dir(self, path: Path) -> bool:
|
|
335
433
|
"""Ensure a directory exists.
|
|
336
434
|
|
|
337
435
|
Args:
|
|
338
436
|
path: Directory path to create.
|
|
437
|
+
|
|
438
|
+
Returns:
|
|
439
|
+
True if directory was created, False if it already existed.
|
|
339
440
|
"""
|
|
441
|
+
if path.exists():
|
|
442
|
+
return False
|
|
443
|
+
|
|
340
444
|
path.mkdir(parents=True, exist_ok=True)
|
|
445
|
+
self.tracker.track(path)
|
|
446
|
+
return True
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
# ============================================================================
|
|
450
|
+
# Home App Creator
|
|
451
|
+
# ============================================================================
|
|
341
452
|
|
|
342
453
|
|
|
343
454
|
class _HomeAppCreator:
|
|
344
455
|
"""Creates the default 'home' Django application."""
|
|
345
456
|
|
|
346
|
-
def __init__(
|
|
457
|
+
def __init__(
|
|
458
|
+
self,
|
|
459
|
+
project_dir: Path,
|
|
460
|
+
writer: _ProjectFileWriter,
|
|
461
|
+
templates: _TemplateManager,
|
|
462
|
+
console: Console,
|
|
463
|
+
):
|
|
347
464
|
"""Initialize the home app creator.
|
|
348
465
|
|
|
349
466
|
Args:
|
|
350
467
|
project_dir: The project directory path.
|
|
351
468
|
writer: File writer instance.
|
|
352
469
|
templates: Template manager instance.
|
|
470
|
+
console: Rich console for output.
|
|
353
471
|
"""
|
|
354
472
|
self.project_dir = project_dir
|
|
355
473
|
self.writer = writer
|
|
356
474
|
self.templates = templates
|
|
475
|
+
self.console = console
|
|
357
476
|
self.home_dir = project_dir / "home"
|
|
358
477
|
|
|
359
478
|
def create(self) -> None:
|
|
360
479
|
"""Create the home app with all necessary files and directories."""
|
|
361
|
-
|
|
480
|
+
# Check if home app already exists
|
|
481
|
+
if self.home_dir.exists():
|
|
482
|
+
self.console.print(
|
|
483
|
+
"[yellow]Home app directory already exists, skipping app creation[/yellow]"
|
|
484
|
+
)
|
|
485
|
+
return
|
|
362
486
|
|
|
363
487
|
# Create the Django app structure
|
|
364
|
-
|
|
488
|
+
self._create_app_structure()
|
|
365
489
|
|
|
366
490
|
# Create app files
|
|
367
491
|
self._create_urls()
|
|
@@ -369,15 +493,36 @@ class _HomeAppCreator:
|
|
|
369
493
|
self._create_templates()
|
|
370
494
|
self._create_static_files()
|
|
371
495
|
|
|
496
|
+
def _create_app_structure(self) -> None:
|
|
497
|
+
"""Create the basic Django app structure using startapp command."""
|
|
498
|
+
from django.core.management import call_command
|
|
499
|
+
|
|
500
|
+
try:
|
|
501
|
+
call_command("startapp", "home")
|
|
502
|
+
# Track the created directory
|
|
503
|
+
self.writer.tracker.track(self.home_dir)
|
|
504
|
+
except Exception as e:
|
|
505
|
+
raise IOError(f"Failed to create home app: {e}") from e
|
|
506
|
+
|
|
372
507
|
def _create_urls(self) -> None:
|
|
373
508
|
"""Create urls.py for the home app."""
|
|
374
509
|
urls_path = self.home_dir / "urls.py"
|
|
375
|
-
self.writer.
|
|
510
|
+
if self.writer.write_to_path_if_not_exists(urls_path, self.templates.home_urls()):
|
|
511
|
+
pass # File was created
|
|
512
|
+
else:
|
|
513
|
+
self.console.print("[dim]urls.py already exists, skipping[/dim]")
|
|
376
514
|
|
|
377
515
|
def _create_views(self) -> None:
|
|
378
516
|
"""Create views.py for the home app."""
|
|
379
517
|
views_path = self.home_dir / "views.py"
|
|
380
|
-
|
|
518
|
+
# Only overwrite if it's the default Django startapp content
|
|
519
|
+
if views_path.exists():
|
|
520
|
+
content = views_path.read_text(encoding="utf-8")
|
|
521
|
+
# Check if it's the default Django views.py (contains only imports or is minimal)
|
|
522
|
+
if len(content.strip()) < 100: # Default Django file is very short
|
|
523
|
+
views_path.write_text(self.templates.home_views().strip(), encoding="utf-8")
|
|
524
|
+
else:
|
|
525
|
+
self.writer.write_to_path_if_not_exists(views_path, self.templates.home_views())
|
|
381
526
|
|
|
382
527
|
def _create_templates(self) -> None:
|
|
383
528
|
"""Create template directory structure and files."""
|
|
@@ -385,7 +530,7 @@ class _HomeAppCreator:
|
|
|
385
530
|
self.writer.ensure_dir(templates_dir)
|
|
386
531
|
|
|
387
532
|
index_path = templates_dir / "index.html"
|
|
388
|
-
self.writer.
|
|
533
|
+
self.writer.write_to_path_if_not_exists(index_path, self.templates.home_index_html())
|
|
389
534
|
|
|
390
535
|
def _create_static_files(self) -> None:
|
|
391
536
|
"""Create static directory structure and CSS files."""
|
|
@@ -393,7 +538,7 @@ class _HomeAppCreator:
|
|
|
393
538
|
self.writer.ensure_dir(static_dir)
|
|
394
539
|
|
|
395
540
|
css_path = static_dir / "tailwind.css"
|
|
396
|
-
self.writer.
|
|
541
|
+
self.writer.write_to_path_if_not_exists(css_path, self.templates.tailwind_css())
|
|
397
542
|
|
|
398
543
|
|
|
399
544
|
# ============================================================================
|
|
@@ -415,6 +560,7 @@ class _ProjectInitializer:
|
|
|
415
560
|
project_dir: Path,
|
|
416
561
|
dependencies: _ProjectDependencies | None = None,
|
|
417
562
|
templates: _TemplateManager | None = None,
|
|
563
|
+
console: Console | None = None,
|
|
418
564
|
):
|
|
419
565
|
"""Initialize the project initializer.
|
|
420
566
|
|
|
@@ -422,22 +568,54 @@ class _ProjectInitializer:
|
|
|
422
568
|
project_dir: The directory where the project will be created.
|
|
423
569
|
dependencies: Dependency manager (uses default if None).
|
|
424
570
|
templates: Template manager (uses default if None).
|
|
571
|
+
console: Rich console for output (creates new if None).
|
|
425
572
|
"""
|
|
426
573
|
self.project_dir = Path(project_dir)
|
|
427
574
|
self.dependencies = dependencies or _ProjectDependencies()
|
|
428
575
|
self.templates = templates or _TemplateManager()
|
|
429
|
-
self.
|
|
576
|
+
self.console = console or Console()
|
|
577
|
+
self.tracker = _FileTracker()
|
|
578
|
+
self.writer = _ProjectFileWriter(self.project_dir, self.console, self.tracker)
|
|
430
579
|
|
|
431
|
-
def _validate_directory(self) -> None:
|
|
580
|
+
def _validate_directory(self, force: bool = False) -> None:
|
|
432
581
|
"""Validate that the directory is suitable for initialization.
|
|
433
582
|
|
|
583
|
+
Args:
|
|
584
|
+
force: Skip validation and proceed regardless.
|
|
585
|
+
|
|
434
586
|
Raises:
|
|
435
|
-
ProjectInitializationError: If directory
|
|
587
|
+
ProjectInitializationError: If directory contains non-VCS files and user declines.
|
|
436
588
|
"""
|
|
437
|
-
if
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
589
|
+
if force:
|
|
590
|
+
return
|
|
591
|
+
|
|
592
|
+
existing_items = list(self.project_dir.iterdir())
|
|
593
|
+
if not existing_items:
|
|
594
|
+
return
|
|
595
|
+
|
|
596
|
+
# Filter out safe items
|
|
597
|
+
problematic_items = [
|
|
598
|
+
item for item in existing_items if item.name not in SAFE_DIRECTORY_ITEMS
|
|
599
|
+
]
|
|
600
|
+
|
|
601
|
+
if not problematic_items:
|
|
602
|
+
# Only safe items present - proceed without prompting
|
|
603
|
+
return
|
|
604
|
+
|
|
605
|
+
# Show what exists and ask for confirmation
|
|
606
|
+
items_list = "\n - ".join(item.name for item in problematic_items)
|
|
607
|
+
|
|
608
|
+
self.console.print(f"[yellow]Directory is not empty. Found:[/yellow]\n - {items_list}\n")
|
|
609
|
+
|
|
610
|
+
should_proceed = Confirm.ask(
|
|
611
|
+
f"Initialize {PKG_DISPLAY_NAME} project anyway? "
|
|
612
|
+
"This will skip existing files and create new ones",
|
|
613
|
+
default=False,
|
|
614
|
+
console=self.console,
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
if not should_proceed:
|
|
618
|
+
raise ProjectInitializationError("Initialization cancelled by user.")
|
|
441
619
|
|
|
442
620
|
def _get_preset_choice(self, preset: str | None = None) -> PresetType:
|
|
443
621
|
"""Get the user's preset choice or validate the provided one.
|
|
@@ -451,8 +629,6 @@ class _ProjectInitializer:
|
|
|
451
629
|
Raises:
|
|
452
630
|
ValueError: If preset is invalid.
|
|
453
631
|
"""
|
|
454
|
-
from rich.prompt import Prompt
|
|
455
|
-
|
|
456
632
|
if preset:
|
|
457
633
|
try:
|
|
458
634
|
return PresetType(preset)
|
|
@@ -460,12 +636,13 @@ class _ProjectInitializer:
|
|
|
460
636
|
valid_presets = [p.value for p in PresetType]
|
|
461
637
|
raise ValueError(
|
|
462
638
|
f"Invalid preset '{preset}'. Must be one of: {', '.join(valid_presets)}"
|
|
463
|
-
)
|
|
639
|
+
) from None
|
|
464
640
|
|
|
465
641
|
choice = Prompt.ask(
|
|
466
642
|
"Choose a preset",
|
|
467
643
|
choices=[p.value for p in PresetType],
|
|
468
644
|
default=PresetType.DEFAULT.value,
|
|
645
|
+
console=self.console,
|
|
469
646
|
)
|
|
470
647
|
return PresetType(choice)
|
|
471
648
|
|
|
@@ -480,79 +657,202 @@ class _ProjectInitializer:
|
|
|
480
657
|
"""
|
|
481
658
|
dependencies = self.dependencies.get_for_preset(preset)
|
|
482
659
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
660
|
+
# Create files only if they don't exist
|
|
661
|
+
files_to_create = {
|
|
662
|
+
"pyproject.toml": self.templates.pyproject_toml(preset, dependencies),
|
|
663
|
+
".gitignore": self.templates.gitignore(),
|
|
664
|
+
"README.md": self.templates.readme(),
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
for filename, content in files_to_create.items():
|
|
668
|
+
if self.writer.write_if_not_exists(filename, content):
|
|
669
|
+
pass # File was created
|
|
670
|
+
else:
|
|
671
|
+
self.console.print(f"[dim]{filename} already exists, skipping[/dim]")
|
|
486
672
|
|
|
487
|
-
def
|
|
673
|
+
def _configure_preset_files_and_env_example(self, preset: PresetType) -> None:
|
|
488
674
|
"""Configure files based on the chosen preset.
|
|
489
675
|
|
|
490
676
|
Args:
|
|
491
677
|
preset: The preset configuration to apply.
|
|
678
|
+
|
|
679
|
+
Raises:
|
|
680
|
+
IOError: If preset-specific file generation fails.
|
|
492
681
|
"""
|
|
493
|
-
|
|
494
|
-
# Import here to avoid circular dependencies
|
|
495
|
-
from .generators import ServerFileGenerator, VercelFileGenerator
|
|
682
|
+
from subprocess import CalledProcessError, run
|
|
496
683
|
|
|
497
|
-
|
|
498
|
-
|
|
684
|
+
try:
|
|
685
|
+
match preset:
|
|
686
|
+
case PresetType.VERCEL:
|
|
687
|
+
# Only generate vercel.json if it doesn't exist
|
|
688
|
+
if not (self.project_dir / "vercel.json").exists():
|
|
689
|
+
run(
|
|
690
|
+
[PKG_NAME, "generate", "-f", "vercel", "-y"],
|
|
691
|
+
cwd=self.project_dir,
|
|
692
|
+
check=True,
|
|
693
|
+
capture_output=True,
|
|
694
|
+
)
|
|
695
|
+
# Track the created file
|
|
696
|
+
self.tracker.track(self.project_dir / "vercel.json")
|
|
697
|
+
else:
|
|
698
|
+
self.console.print("[dim]vercel.json already exists, skipping[/dim]")
|
|
699
|
+
|
|
700
|
+
# Only generate api/server.py if it doesn't exist
|
|
701
|
+
server_path = self.project_dir / "api" / "server.py"
|
|
702
|
+
if not server_path.exists():
|
|
703
|
+
run(
|
|
704
|
+
[PKG_NAME, "generate", "-f", "server", "-y"],
|
|
705
|
+
cwd=self.project_dir,
|
|
706
|
+
check=True,
|
|
707
|
+
capture_output=True,
|
|
708
|
+
)
|
|
709
|
+
# Track the created files
|
|
710
|
+
self.tracker.track(self.project_dir / "api")
|
|
711
|
+
else:
|
|
712
|
+
self.console.print("[dim]api/server.py already exists, skipping[/dim]")
|
|
713
|
+
case _:
|
|
714
|
+
# Future presets will be added here
|
|
715
|
+
pass
|
|
716
|
+
|
|
717
|
+
except CalledProcessError as e:
|
|
718
|
+
error_msg = e.stderr.decode() if e.stderr else str(e)
|
|
719
|
+
raise IOError(f"Failed to generate preset-specific files: {error_msg}") from e
|
|
720
|
+
except FileNotFoundError as e:
|
|
721
|
+
raise IOError(
|
|
722
|
+
f"Command '{PKG_NAME}' not found. Ensure {PKG_DISPLAY_NAME} is properly installed."
|
|
723
|
+
) from e
|
|
724
|
+
|
|
725
|
+
# Only generate .env.example file if it doesn't exist
|
|
726
|
+
env_example_path = self.project_dir / ".env.example"
|
|
727
|
+
if not env_example_path.exists():
|
|
728
|
+
try:
|
|
729
|
+
run(
|
|
730
|
+
[PKG_NAME, "generate", "-f", "env", "-y"],
|
|
731
|
+
cwd=self.project_dir,
|
|
732
|
+
check=True,
|
|
733
|
+
capture_output=True,
|
|
734
|
+
)
|
|
735
|
+
# Track the created file
|
|
736
|
+
self.tracker.track(env_example_path)
|
|
737
|
+
except CalledProcessError as e:
|
|
738
|
+
error_msg = e.stderr.decode() if e.stderr else str(e)
|
|
739
|
+
raise IOError(f"Failed to generate .env file: {error_msg}") from e
|
|
740
|
+
except FileNotFoundError as e:
|
|
741
|
+
raise IOError(
|
|
742
|
+
f"Command '{PKG_NAME}' not found. Ensure {PKG_DISPLAY_NAME} is properly installed."
|
|
743
|
+
) from e
|
|
744
|
+
else:
|
|
745
|
+
self.console.print("[dim].env.example already exists, skipping[/dim]")
|
|
499
746
|
|
|
500
747
|
def _create_home_app(self) -> None:
|
|
501
748
|
"""Create the default home application."""
|
|
502
|
-
home_creator = _HomeAppCreator(self.project_dir, self.writer, self.templates)
|
|
749
|
+
home_creator = _HomeAppCreator(self.project_dir, self.writer, self.templates, self.console)
|
|
503
750
|
home_creator.create()
|
|
504
751
|
|
|
505
|
-
def
|
|
752
|
+
def _show_next_steps(self, preset: PresetType) -> None:
|
|
753
|
+
"""Display next steps for the user after successful initialization.
|
|
754
|
+
|
|
755
|
+
Args:
|
|
756
|
+
preset: The preset that was used.
|
|
757
|
+
"""
|
|
758
|
+
from rich.panel import Panel
|
|
759
|
+
|
|
760
|
+
next_steps = [
|
|
761
|
+
"1. Install dependencies: [bold cyan]uv sync[/bold cyan]",
|
|
762
|
+
"2. Copy [bold].env.example[/bold] to [bold].env[/bold] and configure",
|
|
763
|
+
]
|
|
764
|
+
|
|
765
|
+
if preset == PresetType.VERCEL:
|
|
766
|
+
next_steps.append(
|
|
767
|
+
"3. Configure Vercel blob token in [bold]pyproject.toml[/bold] or [bold].env[/bold]"
|
|
768
|
+
)
|
|
769
|
+
next_steps.append("4. Run development server: [bold cyan]djx runserver[/bold cyan]")
|
|
770
|
+
else:
|
|
771
|
+
next_steps.append("3. Run development server: [bold cyan]djx runserver[/bold cyan]")
|
|
772
|
+
|
|
773
|
+
panel = Panel(
|
|
774
|
+
"\n".join(next_steps),
|
|
775
|
+
title=f"[bold green]✓ {PKG_DISPLAY_NAME} project initialized successfully![/bold green]",
|
|
776
|
+
border_style="green",
|
|
777
|
+
padding=(1, 2),
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
self.console.print("\n")
|
|
781
|
+
self.console.print(panel)
|
|
782
|
+
|
|
783
|
+
def create(self, preset: str | None = None, force: bool = False) -> ExitCode:
|
|
506
784
|
"""Execute the full project initialization workflow.
|
|
507
785
|
|
|
508
786
|
Args:
|
|
509
787
|
preset: Optional preset to use without prompting.
|
|
788
|
+
force: Skip directory validation.
|
|
510
789
|
|
|
511
790
|
Returns:
|
|
512
791
|
ExitCode indicating success or failure.
|
|
513
792
|
"""
|
|
514
|
-
from christianwhocodes.io import Text, print
|
|
515
|
-
|
|
516
793
|
try:
|
|
517
|
-
# Validate directory is empty
|
|
518
|
-
self._validate_directory()
|
|
794
|
+
# Validate directory is empty or acceptable
|
|
795
|
+
self._validate_directory(force=force)
|
|
519
796
|
|
|
520
797
|
# Get and validate preset choice
|
|
521
798
|
chosen_preset = self._get_preset_choice(preset)
|
|
522
799
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
800
|
+
with Progress(
|
|
801
|
+
SpinnerColumn(),
|
|
802
|
+
TextColumn("[progress.description]{task.description}"),
|
|
803
|
+
console=self.console,
|
|
804
|
+
transient=True,
|
|
805
|
+
) as progress:
|
|
806
|
+
# Create core configuration files
|
|
807
|
+
task = progress.add_task("Creating project files...", total=None)
|
|
808
|
+
self._create_core_files(chosen_preset)
|
|
809
|
+
progress.update(task, completed=True)
|
|
810
|
+
|
|
811
|
+
# Configure preset-specific files
|
|
812
|
+
task = progress.add_task("Configuring preset files...", total=None)
|
|
813
|
+
self._configure_preset_files_and_env_example(chosen_preset)
|
|
814
|
+
progress.update(task, completed=True)
|
|
815
|
+
|
|
816
|
+
# Create default app
|
|
817
|
+
task = progress.add_task("Creating home app...", total=None)
|
|
818
|
+
self._create_home_app()
|
|
819
|
+
progress.update(task, completed=True)
|
|
820
|
+
|
|
821
|
+
self._show_next_steps(chosen_preset)
|
|
536
822
|
return ExitCode.SUCCESS
|
|
537
823
|
|
|
538
824
|
except KeyboardInterrupt:
|
|
539
|
-
|
|
825
|
+
self.tracker.cleanup_all()
|
|
826
|
+
self.console.print(
|
|
827
|
+
"\n[yellow]Project initialization cancelled. Cleaned up partial files.[/yellow]"
|
|
828
|
+
)
|
|
540
829
|
return ExitCode.ERROR
|
|
541
830
|
|
|
542
831
|
except ProjectInitializationError as e:
|
|
543
|
-
|
|
832
|
+
# User declined to proceed - no cleanup needed since nothing was created
|
|
833
|
+
self.console.print(f"[yellow]{e}[/yellow]")
|
|
544
834
|
return ExitCode.ERROR
|
|
545
835
|
|
|
546
836
|
except ValueError as e:
|
|
547
|
-
|
|
837
|
+
self.tracker.cleanup_all()
|
|
838
|
+
self.console.print(
|
|
839
|
+
f"[red]Configuration error:[/red] {e}\n[yellow]Cleaned up partial files.[/yellow]"
|
|
840
|
+
)
|
|
548
841
|
return ExitCode.ERROR
|
|
549
842
|
|
|
550
843
|
except (IOError, PermissionError) as e:
|
|
551
|
-
|
|
844
|
+
self.tracker.cleanup_all()
|
|
845
|
+
self.console.print(
|
|
846
|
+
f"[red]File system error:[/red] {e}\n[yellow]Cleaned up partial files.[/yellow]"
|
|
847
|
+
)
|
|
552
848
|
return ExitCode.ERROR
|
|
553
849
|
|
|
554
850
|
except Exception as e:
|
|
555
|
-
|
|
851
|
+
self.tracker.cleanup_all()
|
|
852
|
+
self.console.print(
|
|
853
|
+
f"[red]Unexpected error during initialization:[/red] {e}\n"
|
|
854
|
+
f"[yellow]Cleaned up partial files.[/yellow]"
|
|
855
|
+
)
|
|
556
856
|
return ExitCode.ERROR
|
|
557
857
|
|
|
558
858
|
|
|
@@ -561,13 +861,22 @@ class _ProjectInitializer:
|
|
|
561
861
|
# ============================================================================
|
|
562
862
|
|
|
563
863
|
|
|
564
|
-
def initialize(preset: str | None = None) -> ExitCode:
|
|
864
|
+
def initialize(preset: str | None = None, force: bool = False) -> ExitCode:
|
|
565
865
|
"""Main entry point for project initialization.
|
|
566
866
|
|
|
867
|
+
Creates a new project with the specified preset configuration.
|
|
868
|
+
|
|
567
869
|
Args:
|
|
568
870
|
preset: Optional preset to use without prompting.
|
|
871
|
+
Available presets: 'default', 'vercel'.
|
|
872
|
+
force: Skip directory validation and proceed even if directory is not empty.
|
|
569
873
|
|
|
570
874
|
Returns:
|
|
571
|
-
ExitCode
|
|
875
|
+
ExitCode.SUCCESS if initialization completed successfully,
|
|
876
|
+
ExitCode.ERROR otherwise.
|
|
877
|
+
|
|
878
|
+
Example:
|
|
879
|
+
>>> initialize(preset="vercel")
|
|
880
|
+
>>> initialize(force=True)
|
|
572
881
|
"""
|
|
573
|
-
return _ProjectInitializer(PROJECT_DIR).create(preset=preset)
|
|
882
|
+
return _ProjectInitializer(PROJECT_DIR).create(preset=preset, force=force)
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
from enum import StrEnum
|
|
5
5
|
|
|
6
6
|
from ... import (
|
|
7
|
+
INCLUDE_PROJECT_MAIN_APP,
|
|
7
8
|
PKG_API_NAME,
|
|
8
9
|
PKG_MANAGEMENT_NAME,
|
|
9
10
|
PKG_NAME,
|
|
@@ -62,7 +63,7 @@ def _get_installed_apps() -> list[str]:
|
|
|
62
63
|
"""
|
|
63
64
|
|
|
64
65
|
base_apps: list[str] = [
|
|
65
|
-
PROJECT_MAIN_APP_NAME,
|
|
66
|
+
*([PROJECT_MAIN_APP_NAME] if INCLUDE_PROJECT_MAIN_APP else []),
|
|
66
67
|
PKG_NAME,
|
|
67
68
|
f"{PKG_NAME}.{PKG_API_NAME}",
|
|
68
69
|
f"{PKG_NAME}.{PKG_UI_NAME}",
|
|
@@ -11,5 +11,9 @@ urlpatterns: list[URLPattern | URLResolver] = [
|
|
|
11
11
|
),
|
|
12
12
|
path("api/", include(f"{PKG_NAME}.api.urls")),
|
|
13
13
|
path("ui/", include(f"{PKG_NAME}.ui.urls")),
|
|
14
|
-
|
|
14
|
+
*(
|
|
15
|
+
[path("", include(f"{PROJECT_MAIN_APP_NAME}.urls"))]
|
|
16
|
+
if PROJECT_MAIN_APP_NAME in INSTALLED_APPS
|
|
17
|
+
else []
|
|
18
|
+
),
|
|
15
19
|
]
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
from enum import StrEnum
|
|
2
|
-
from typing import Any
|
|
3
|
-
|
|
4
|
-
from christianwhocodes.generators import (
|
|
5
|
-
FileGenerator,
|
|
6
|
-
FileGeneratorOption,
|
|
7
|
-
PgPassFileGenerator,
|
|
8
|
-
PgServiceFileGenerator,
|
|
9
|
-
SSHConfigFileGenerator,
|
|
10
|
-
)
|
|
11
|
-
from django.core.management.base import BaseCommand, CommandParser
|
|
12
|
-
|
|
13
|
-
from .generators import EnvFileGenerator, ServerFileGenerator, VercelFileGenerator
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class FileOption(StrEnum):
|
|
17
|
-
PG_SERVICE = FileGeneratorOption.PG_SERVICE.value
|
|
18
|
-
PGPASS = FileGeneratorOption.PGPASS.value
|
|
19
|
-
SSH_CONFIG = FileGeneratorOption.SSH_CONFIG.value
|
|
20
|
-
ENV = "env"
|
|
21
|
-
SERVER = "server"
|
|
22
|
-
VERCEL = "vercel"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class Command(BaseCommand):
|
|
26
|
-
help: str = "Generate configuration files (e.g., .env.example, vercel.json, asgi.py, wsgi.py, .pg_service.conf, pgpass.conf / .pgpass, ssh config)."
|
|
27
|
-
|
|
28
|
-
def add_arguments(self, parser: CommandParser) -> None:
|
|
29
|
-
parser.add_argument(
|
|
30
|
-
"-f",
|
|
31
|
-
"--file",
|
|
32
|
-
dest="file",
|
|
33
|
-
choices=[opt.value for opt in FileOption],
|
|
34
|
-
type=FileOption,
|
|
35
|
-
required=True,
|
|
36
|
-
help=f"Specify which file to generate (options: {', '.join(o.value for o in FileOption)}).",
|
|
37
|
-
)
|
|
38
|
-
parser.add_argument(
|
|
39
|
-
"-y",
|
|
40
|
-
"--force",
|
|
41
|
-
dest="force",
|
|
42
|
-
action="store_true",
|
|
43
|
-
help="Force overwrite without confirmation.",
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
def handle(self, *args: Any, **options: Any) -> None:
|
|
47
|
-
file_option: FileOption = FileOption(options["file"])
|
|
48
|
-
force: bool = options["force"]
|
|
49
|
-
|
|
50
|
-
generators: dict[FileOption, type[FileGenerator]] = {
|
|
51
|
-
FileOption.VERCEL: VercelFileGenerator,
|
|
52
|
-
FileOption.SERVER: ServerFileGenerator,
|
|
53
|
-
FileOption.PG_SERVICE: PgServiceFileGenerator,
|
|
54
|
-
FileOption.PGPASS: PgPassFileGenerator,
|
|
55
|
-
FileOption.SSH_CONFIG: SSHConfigFileGenerator,
|
|
56
|
-
FileOption.ENV: EnvFileGenerator,
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
generator_class: type[FileGenerator] = generators[file_option]
|
|
60
|
-
generator = generator_class()
|
|
61
|
-
generator.create(force=force)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
from .file import * # noqa: F403
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|