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 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"Are you currently executing in a {PKG_DISPLAY_NAME} project base directory?\n"
211
- f"If not, navigate to your project's root or create a new {PKG_DISPLAY_NAME} project to run the command.\n\n"
212
- "A valid project requires:\n"
213
- f" - `pyproject.toml` file with a 'tool.{PKG_NAME}' section (even if empty)\n"
214
- f"Validation failed: {e}",
215
- Text.WARNING,
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
@@ -0,0 +1,8 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ class DatabaseBackend(StrEnum):
5
+ """Available database backends."""
6
+
7
+ SQLITE3 = "sqlite3"
8
+ POSTGRESQL = "postgresql"
@@ -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=["sqlite3", "postgresql"],
17
+ choices=[DatabaseBackend.SQLITE3, DatabaseBackend.POSTGRESQL],
17
18
  env="DB_BACKEND",
18
19
  toml="db.backend",
19
- default="sqlite3",
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 "sqlite" | "sqlite3":
83
+ case DatabaseBackend.SQLITE3:
83
84
  return {
84
85
  "default": {
85
- "ENGINE": "django.db.backends.sqlite3",
86
- "NAME": PROJECT_DIR / "db.sqlite3",
86
+ "ENGINE": f"django.db.backends.{DatabaseBackend.SQLITE3.value}",
87
+ "NAME": PROJECT_DIR / f"db.{DatabaseBackend.SQLITE3.value}",
87
88
  }
88
89
  }
89
- case "postgresql" | "postgres" | "psql" | "pgsql" | "pg" | "psycopg":
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.postgresql",
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.postgresql",
110
+ "ENGINE": f"django.db.backends.{DatabaseBackend.POSTGRESQL.value}",
110
111
  "NAME": _DATABASE.name,
111
112
  "OPTIONS": options,
112
113
  }
djangx/enums.py ADDED
@@ -0,0 +1,2 @@
1
+ from .api.enums import * # noqa: F403
2
+ from .management.enums import * # noqa: F403
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
- from enum import StrEnum
2
- from typing import Any
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 .generators import EnvFileGenerator, ServerFileGenerator, VercelFileGenerator
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 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"
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: VercelFileGenerator,
52
- FileOption.SERVER: ServerFileGenerator,
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: EnvFileGenerator,
255
+ FileOption.ENV: _EnvFileGenerator,
57
256
  }
58
257
 
59
258
  generator_class: type[FileGenerator] = generators[file_option]