djangx 1.5.5__py3-none-any.whl → 1.5.7__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.
- djangx/__init__.py +14 -6
- djangx/api/enums.py +8 -0
- djangx/api/settings/databases.py +9 -8
- djangx/enums.py +2 -0
- djangx/management/cli.py +14 -1
- djangx/management/commands/generate.py +213 -14
- djangx/management/commands/startproject.py +562 -115
- djangx/management/enums.py +63 -0
- djangx/management/settings/apps.py +41 -83
- djangx/management/urls.py +5 -1
- {djangx-1.5.5.dist-info → djangx-1.5.7.dist-info}/METADATA +2 -1
- {djangx-1.5.5.dist-info → djangx-1.5.7.dist-info}/RECORD +14 -13
- djangx/management/commands/generators/__init__.py +0 -1
- djangx/management/commands/generators/file.py +0 -217
- {djangx-1.5.5.dist-info → djangx-1.5.7.dist-info}/WHEEL +0 -0
- {djangx-1.5.5.dist-info → djangx-1.5.7.dist-info}/entry_points.txt +0 -0
djangx/__init__.py
CHANGED
|
@@ -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
|
djangx/api/enums.py
ADDED
djangx/api/settings/databases.py
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
# https://www.postgresql.org/docs/current/libpq-pgpass.html
|
|
6
6
|
# ==============================================================================
|
|
7
7
|
from ... import PROJECT_DIR, Conf, ConfField
|
|
8
|
+
from ..enums import DatabaseBackend
|
|
8
9
|
from ..types import DatabaseDict, DatabaseOptionsDict, DatabasesDict
|
|
9
10
|
|
|
10
11
|
|
|
@@ -13,10 +14,10 @@ class DatabaseConf(Conf):
|
|
|
13
14
|
|
|
14
15
|
backend = ConfField(
|
|
15
16
|
type=str,
|
|
16
|
-
choices=[
|
|
17
|
+
choices=[DatabaseBackend.SQLITE3, DatabaseBackend.POSTGRESQL],
|
|
17
18
|
env="DB_BACKEND",
|
|
18
19
|
toml="db.backend",
|
|
19
|
-
default=
|
|
20
|
+
default=DatabaseBackend.SQLITE3,
|
|
20
21
|
)
|
|
21
22
|
# postgresql specific
|
|
22
23
|
use_vars = ConfField(
|
|
@@ -79,14 +80,14 @@ def _get_databases_config() -> DatabasesDict:
|
|
|
79
80
|
backend: str = _DATABASE.backend.lower()
|
|
80
81
|
|
|
81
82
|
match backend:
|
|
82
|
-
case
|
|
83
|
+
case DatabaseBackend.SQLITE3:
|
|
83
84
|
return {
|
|
84
85
|
"default": {
|
|
85
|
-
"ENGINE": "django.db.backends.
|
|
86
|
-
"NAME": PROJECT_DIR / "db.
|
|
86
|
+
"ENGINE": f"django.db.backends.{DatabaseBackend.SQLITE3.value}",
|
|
87
|
+
"NAME": PROJECT_DIR / f"db.{DatabaseBackend.SQLITE3.value}",
|
|
87
88
|
}
|
|
88
89
|
}
|
|
89
|
-
case
|
|
90
|
+
case DatabaseBackend.POSTGRESQL:
|
|
90
91
|
options: DatabaseOptionsDict = {
|
|
91
92
|
"pool": _DATABASE.pool,
|
|
92
93
|
"sslmode": _DATABASE.ssl_mode,
|
|
@@ -95,7 +96,7 @@ def _get_databases_config() -> DatabasesDict:
|
|
|
95
96
|
# Add service or connection vars
|
|
96
97
|
if _DATABASE.use_vars:
|
|
97
98
|
config: DatabaseDict = {
|
|
98
|
-
"ENGINE": "django.db.backends.
|
|
99
|
+
"ENGINE": f"django.db.backends.{DatabaseBackend.POSTGRESQL.value}",
|
|
99
100
|
"NAME": _DATABASE.name,
|
|
100
101
|
"USER": _DATABASE.user,
|
|
101
102
|
"PASSWORD": _DATABASE.password,
|
|
@@ -106,7 +107,7 @@ def _get_databases_config() -> DatabasesDict:
|
|
|
106
107
|
else:
|
|
107
108
|
options["service"] = _DATABASE.service
|
|
108
109
|
config: DatabaseDict = {
|
|
109
|
-
"ENGINE": "django.db.backends.
|
|
110
|
+
"ENGINE": f"django.db.backends.{DatabaseBackend.POSTGRESQL.value}",
|
|
110
111
|
"NAME": _DATABASE.name,
|
|
111
112
|
"OPTIONS": options,
|
|
112
113
|
}
|
djangx/enums.py
ADDED
djangx/management/cli.py
CHANGED
|
@@ -17,6 +17,7 @@ def main() -> Optional[NoReturn]:
|
|
|
17
17
|
case "startproject" | "init" | "new":
|
|
18
18
|
from argparse import ArgumentParser, Namespace
|
|
19
19
|
|
|
20
|
+
from ..enums import DatabaseBackend
|
|
20
21
|
from .commands.startproject import initialize
|
|
21
22
|
|
|
22
23
|
parser = ArgumentParser(description=f"Initialize a new {PKG_DISPLAY_NAME} project")
|
|
@@ -25,9 +26,21 @@ def main() -> Optional[NoReturn]:
|
|
|
25
26
|
choices=["default", "vercel"],
|
|
26
27
|
help="Project preset to use (skips interactive prompt)",
|
|
27
28
|
)
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"--database",
|
|
31
|
+
"--db",
|
|
32
|
+
choices=[DatabaseBackend.SQLITE3, DatabaseBackend.POSTGRESQL],
|
|
33
|
+
help=f"Database backend to use (skips interactive prompt). Note: Vercel preset requires {DatabaseBackend.POSTGRESQL.value}.",
|
|
34
|
+
)
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
"--force",
|
|
37
|
+
"-f",
|
|
38
|
+
action="store_true",
|
|
39
|
+
help="Skip directory validation and initialize even if directory is not empty",
|
|
40
|
+
)
|
|
28
41
|
args: Namespace = parser.parse_args(sys.argv[2:])
|
|
29
42
|
|
|
30
|
-
sys.exit(initialize(preset=args.preset))
|
|
43
|
+
sys.exit(initialize(preset=args.preset, database=args.database, force=args.force))
|
|
31
44
|
|
|
32
45
|
case _:
|
|
33
46
|
from os import environ
|
|
@@ -1,25 +1,224 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import builtins
|
|
2
|
+
import pathlib
|
|
3
|
+
from typing import Any, Optional, cast
|
|
3
4
|
|
|
4
5
|
from christianwhocodes.generators import (
|
|
5
6
|
FileGenerator,
|
|
6
|
-
FileGeneratorOption,
|
|
7
7
|
PgPassFileGenerator,
|
|
8
8
|
PgServiceFileGenerator,
|
|
9
9
|
SSHConfigFileGenerator,
|
|
10
10
|
)
|
|
11
11
|
from django.core.management.base import BaseCommand, CommandParser
|
|
12
12
|
|
|
13
|
-
from
|
|
13
|
+
from ... import PKG_DISPLAY_NAME, PKG_NAME, PROJECT_API_DIR, PROJECT_DIR, Conf
|
|
14
|
+
from ..enums import FileOption
|
|
14
15
|
|
|
15
16
|
|
|
16
|
-
class
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
class _ServerFileGenerator(FileGenerator):
|
|
18
|
+
f"""
|
|
19
|
+
Generator for ASGI / WSGI configuration in api/server.py file.
|
|
20
|
+
|
|
21
|
+
Creates an server.py file in the /api directory.
|
|
22
|
+
Required for running {PKG_DISPLAY_NAME} apps with ASGI or WSGI servers.
|
|
23
|
+
Note that the type of api gateway dependes on the SERVER_USE_ASGI setting.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def file_path(self) -> pathlib.Path:
|
|
28
|
+
"""Return the path for the api/server.py"""
|
|
29
|
+
return PROJECT_API_DIR / "server.py"
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def data(self) -> str:
|
|
33
|
+
"""Return template content for api/server.py."""
|
|
34
|
+
return f"from {PKG_NAME}.api.backends.server import application\n\napp = application\n"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class _VercelFileGenerator(FileGenerator):
|
|
38
|
+
"""
|
|
39
|
+
Generator for Vercel configuration file (vercel.json).
|
|
40
|
+
|
|
41
|
+
Creates a vercel.json file in the base directory.
|
|
42
|
+
Useful for deploying to Vercel with custom install/build commands.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def file_path(self) -> pathlib.Path:
|
|
47
|
+
return PROJECT_DIR / "vercel.json"
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def data(self) -> str:
|
|
51
|
+
"""Return template content for vercel.json."""
|
|
52
|
+
lines = [
|
|
53
|
+
"{",
|
|
54
|
+
' "$schema": "https://openapi.vercel.sh/vercel.json",',
|
|
55
|
+
f' "installCommand": "uv run {PKG_NAME} runinstall",',
|
|
56
|
+
f' "buildCommand": "uv run {PKG_NAME} runbuild",',
|
|
57
|
+
' "rewrites": [',
|
|
58
|
+
" {",
|
|
59
|
+
' "source": "/(.*)",',
|
|
60
|
+
' "destination": "/api/server"',
|
|
61
|
+
" }",
|
|
62
|
+
" ]",
|
|
63
|
+
"}",
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
return "\n".join(lines) + "\n"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class _EnvFileGenerator(FileGenerator):
|
|
70
|
+
"""
|
|
71
|
+
Generator for environment configuration file (.env.example).
|
|
72
|
+
|
|
73
|
+
Creates a .env.example file in the base directory with all
|
|
74
|
+
possible environment variables from configuration classes.
|
|
75
|
+
All variables are commented out by default.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def file_path(self) -> pathlib.Path:
|
|
80
|
+
"""Return the path for the .env.example file."""
|
|
81
|
+
return PROJECT_DIR / ".env.example"
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def data(self) -> str:
|
|
85
|
+
"""Generate .env file content based on all ConfFields from Conf subclasses."""
|
|
86
|
+
|
|
87
|
+
lines: list[str] = []
|
|
88
|
+
|
|
89
|
+
# Add header
|
|
90
|
+
lines.extend(self._add_header())
|
|
91
|
+
|
|
92
|
+
# Get all fields from Conf subclasses
|
|
93
|
+
env_fields = Conf.get_env_fields()
|
|
94
|
+
|
|
95
|
+
# Group fields by class
|
|
96
|
+
fields_by_class: dict[str, list[dict[str, Any]]] = {}
|
|
97
|
+
for field in env_fields:
|
|
98
|
+
class_name = cast(str, field["class"])
|
|
99
|
+
if class_name not in fields_by_class:
|
|
100
|
+
fields_by_class[class_name] = []
|
|
101
|
+
fields_by_class[class_name].append(field)
|
|
102
|
+
|
|
103
|
+
# Generate content for each class group
|
|
104
|
+
for class_name in sorted(fields_by_class.keys()):
|
|
105
|
+
fields = fields_by_class[class_name]
|
|
106
|
+
|
|
107
|
+
# Add section header
|
|
108
|
+
lines.extend(self._add_section_header(class_name))
|
|
109
|
+
|
|
110
|
+
# Process each field in this class
|
|
111
|
+
for field in fields:
|
|
112
|
+
env_var = field["env"]
|
|
113
|
+
toml_key = field["toml"]
|
|
114
|
+
choices_key = field["choices"]
|
|
115
|
+
default_value = field["default"]
|
|
116
|
+
field_type = field["type"]
|
|
117
|
+
|
|
118
|
+
# Add field documentation with proper format hints
|
|
119
|
+
lines.append(
|
|
120
|
+
f"# Variable: {self._format_variable_hint(env_var, choices_key, field_type)}"
|
|
121
|
+
)
|
|
122
|
+
if toml_key:
|
|
123
|
+
lines.append(f"# TOML Key: {toml_key}")
|
|
124
|
+
|
|
125
|
+
# Format default value for display
|
|
126
|
+
if default_value is not None:
|
|
127
|
+
formatted_default = self._format_default_value(default_value, field_type)
|
|
128
|
+
lines.append(f"# Default: {formatted_default}")
|
|
129
|
+
else:
|
|
130
|
+
lines.append("# Default: (none)")
|
|
131
|
+
|
|
132
|
+
lines.append(f"# {env_var}=")
|
|
133
|
+
|
|
134
|
+
lines.append("")
|
|
135
|
+
|
|
136
|
+
lines.append("")
|
|
137
|
+
|
|
138
|
+
# Add footer
|
|
139
|
+
lines.extend(self._add_footer())
|
|
140
|
+
|
|
141
|
+
return "\n".join(lines)
|
|
142
|
+
|
|
143
|
+
def _add_header(self) -> list[str]:
|
|
144
|
+
"""Add header to the .env.example file."""
|
|
145
|
+
lines: list[str] = []
|
|
146
|
+
lines.append("# " + "=" * 78)
|
|
147
|
+
lines.append(f"# {PKG_DISPLAY_NAME} Environment Configuration")
|
|
148
|
+
lines.append("# " + "=" * 78)
|
|
149
|
+
lines.append("#")
|
|
150
|
+
lines.append("# This file contains all available environment variables for configuration.")
|
|
151
|
+
lines.append("#")
|
|
152
|
+
lines.append("# Configuration Priority: ENV > TOML > Default")
|
|
153
|
+
lines.append("# " + "=" * 78)
|
|
154
|
+
lines.append("")
|
|
155
|
+
return lines
|
|
156
|
+
|
|
157
|
+
def _add_section_header(self, class_name: str) -> list[str]:
|
|
158
|
+
"""Add section header for a configuration class."""
|
|
159
|
+
lines: list[str] = []
|
|
160
|
+
lines.append("# " + "-" * 78)
|
|
161
|
+
lines.append(f"# {class_name} Configuration")
|
|
162
|
+
lines.append("# " + "-" * 78)
|
|
163
|
+
lines.append("")
|
|
164
|
+
return lines
|
|
165
|
+
|
|
166
|
+
def _add_footer(self) -> list[str]:
|
|
167
|
+
"""Add footer to the .env.example file."""
|
|
168
|
+
lines: list[str] = []
|
|
169
|
+
lines.append("# " + "=" * 78)
|
|
170
|
+
lines.append("# End of Configuration")
|
|
171
|
+
lines.append("# " + "=" * 78)
|
|
172
|
+
return lines
|
|
173
|
+
|
|
174
|
+
def _format_choices(self, choices: list[str]) -> str:
|
|
175
|
+
"""Format choices as 'choice1' | 'choice2' | 'choice3'."""
|
|
176
|
+
return " | ".join(f'"{choice}"' for choice in choices)
|
|
177
|
+
|
|
178
|
+
def _get_type_example(self, field_type: type) -> str:
|
|
179
|
+
"""Get example value for a field type."""
|
|
180
|
+
match field_type:
|
|
181
|
+
case builtins.bool:
|
|
182
|
+
return '"true" | "false"'
|
|
183
|
+
case builtins.int:
|
|
184
|
+
return '"123"'
|
|
185
|
+
case builtins.float:
|
|
186
|
+
return '"123.45"'
|
|
187
|
+
case builtins.list:
|
|
188
|
+
return '"value1,value2,value3"'
|
|
189
|
+
case pathlib.Path:
|
|
190
|
+
return '"/full/path/to/something"'
|
|
191
|
+
case _:
|
|
192
|
+
return '"value"'
|
|
193
|
+
|
|
194
|
+
def _format_variable_hint(
|
|
195
|
+
self, env_var: str, choices_key: Optional[list[str]], field_type: type
|
|
196
|
+
) -> str:
|
|
197
|
+
"""Format variable hint showing proper syntax based on type."""
|
|
198
|
+
if choices_key:
|
|
199
|
+
return f"{env_var}={self._format_choices(choices_key)}"
|
|
200
|
+
else:
|
|
201
|
+
return f"{env_var}={self._get_type_example(field_type)}"
|
|
202
|
+
|
|
203
|
+
def _format_default_value(self, value: Any, field_type: type) -> str:
|
|
204
|
+
"""Format default value for display in comments."""
|
|
205
|
+
if value is None:
|
|
206
|
+
return "(none)"
|
|
207
|
+
|
|
208
|
+
match field_type:
|
|
209
|
+
case builtins.bool:
|
|
210
|
+
return "true" if value else "false"
|
|
211
|
+
case builtins.list:
|
|
212
|
+
if isinstance(value, list):
|
|
213
|
+
list_items = cast(list[Any], value)
|
|
214
|
+
if not list_items:
|
|
215
|
+
return "(empty list)"
|
|
216
|
+
return ",".join(str(v) for v in list_items)
|
|
217
|
+
return str(value)
|
|
218
|
+
case pathlib.Path:
|
|
219
|
+
return str(pathlib.PurePosixPath(value))
|
|
220
|
+
case _:
|
|
221
|
+
return str(value)
|
|
23
222
|
|
|
24
223
|
|
|
25
224
|
class Command(BaseCommand):
|
|
@@ -48,12 +247,12 @@ class Command(BaseCommand):
|
|
|
48
247
|
force: bool = options["force"]
|
|
49
248
|
|
|
50
249
|
generators: dict[FileOption, type[FileGenerator]] = {
|
|
51
|
-
FileOption.VERCEL:
|
|
52
|
-
FileOption.SERVER:
|
|
250
|
+
FileOption.VERCEL: _VercelFileGenerator,
|
|
251
|
+
FileOption.SERVER: _ServerFileGenerator,
|
|
53
252
|
FileOption.PG_SERVICE: PgServiceFileGenerator,
|
|
54
253
|
FileOption.PGPASS: PgPassFileGenerator,
|
|
55
254
|
FileOption.SSH_CONFIG: SSHConfigFileGenerator,
|
|
56
|
-
FileOption.ENV:
|
|
255
|
+
FileOption.ENV: _EnvFileGenerator,
|
|
57
256
|
}
|
|
58
257
|
|
|
59
258
|
generator_class: type[FileGenerator] = generators[file_option]
|