djinitx 0.3.0__tar.gz → 0.3.2__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.
Files changed (98) hide show
  1. {djinitx-0.3.0 → djinitx-0.3.2}/.github/workflows/publish.yml +2 -2
  2. {djinitx-0.3.0 → djinitx-0.3.2}/PKG-INFO +3 -5
  3. {djinitx-0.3.0 → djinitx-0.3.2}/TODO.md +2 -1
  4. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/cli.py +8 -8
  5. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/commands/handler.py +7 -17
  6. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/services/app.py +38 -14
  7. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/services/files.py +98 -66
  8. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/services/project.py +22 -19
  9. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/ui/console.py +19 -10
  10. djinitx-0.3.2/djinit/ui/input.py +337 -0
  11. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/utils/common.py +22 -0
  12. djinitx-0.3.0/djinit/utils/security.py → djinitx-0.3.2/djinit/utils/secretkey.py +1 -21
  13. {djinitx-0.3.0 → djinitx-0.3.2}/pyproject.toml +9 -5
  14. djinitx-0.3.2/requirements.txt +7 -0
  15. {djinitx-0.3.0 → djinitx-0.3.2}/setup.py +2 -1
  16. {djinitx-0.3.0 → djinitx-0.3.2}/uv.lock +118 -27
  17. djinitx-0.3.0/djinit/ui/input.py +0 -597
  18. djinitx-0.3.0/requirements.txt +0 -4
  19. {djinitx-0.3.0 → djinitx-0.3.2}/.github/dependabot.yml +0 -0
  20. {djinitx-0.3.0 → djinitx-0.3.2}/.github/workflows/ci.yml +0 -0
  21. {djinitx-0.3.0 → djinitx-0.3.2}/.github/workflows/codeql.yml +0 -0
  22. {djinitx-0.3.0 → djinitx-0.3.2}/.github/workflows/dependabot-auto-merge.yml +0 -0
  23. {djinitx-0.3.0 → djinitx-0.3.2}/.github/workflows/pre-commit.yml +0 -0
  24. {djinitx-0.3.0 → djinitx-0.3.2}/.gitignore +0 -0
  25. {djinitx-0.3.0 → djinitx-0.3.2}/.pre-commit-config.yaml +0 -0
  26. {djinitx-0.3.0 → djinitx-0.3.2}/.python-version +0 -0
  27. {djinitx-0.3.0 → djinitx-0.3.2}/LICENSE +0 -0
  28. {djinitx-0.3.0 → djinitx-0.3.2}/MANIFEST.in +0 -0
  29. {djinitx-0.3.0 → djinitx-0.3.2}/README.md +0 -0
  30. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/__init__.py +0 -0
  31. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/commands/__init__.py +0 -0
  32. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/core/__init__.py +0 -0
  33. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/core/config.py +0 -0
  34. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/core/types.py +0 -0
  35. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/generator.py +0 -0
  36. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/services/__init__.py +0 -0
  37. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/services/templates.py +0 -0
  38. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/components/admin.j2 +0 -0
  39. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/components/api_readme.j2 +0 -0
  40. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/components/apps.j2 +0 -0
  41. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/components/migrations_init.j2 +0 -0
  42. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/components/models.j2 +0 -0
  43. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/components/models_readme.j2 +0 -0
  44. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/components/routes.j2 +0 -0
  45. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/components/serializers.j2 +0 -0
  46. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/components/tests.j2 +0 -0
  47. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/components/urls.j2 +0 -0
  48. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/components/views.j2 +0 -0
  49. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/config/asgi.j2 +0 -0
  50. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/config/settings/base.j2 +0 -0
  51. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/config/settings/development.j2 +0 -0
  52. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/config/settings/production.j2 +0 -0
  53. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/config/urls/base.j2 +0 -0
  54. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/config/urls/with_api.j2 +0 -0
  55. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/config/urls/with_apps.j2 +0 -0
  56. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/config/wsgi.j2 +0 -0
  57. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/presets/predefined/api/urls.j2 +0 -0
  58. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/presets/predefined/api/v1/urls.j2 +0 -0
  59. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/presets/predefined/apps/generic/models.j2 +0 -0
  60. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/presets/predefined/apps/generic/serializers.j2 +0 -0
  61. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/presets/predefined/apps/generic/services.j2 +0 -0
  62. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/presets/predefined/apps/generic/tests.j2 +0 -0
  63. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/presets/predefined/apps/generic/urls.j2 +0 -0
  64. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/presets/predefined/apps/generic/views.j2 +0 -0
  65. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/presets/predefined/apps/users/models.j2 +0 -0
  66. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/presets/predefined/apps/users/serializers.j2 +0 -0
  67. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/presets/predefined/apps/users/services.j2 +0 -0
  68. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/presets/predefined/apps/users/tests.j2 +0 -0
  69. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/presets/predefined/apps/users/urls.j2 +0 -0
  70. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/presets/predefined/apps/users/views.j2 +0 -0
  71. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/presets/predefined/core/exceptions.j2 +0 -0
  72. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/presets/predefined/core/middleware/request_logger.j2 +0 -0
  73. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/presets/predefined/core/mixins/timestamped_model.j2 +0 -0
  74. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/presets/predefined/core/utils/responses.j2 +0 -0
  75. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/presets/unified/apps/core/apps.j2 +0 -0
  76. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/presets/unified/apps/core/models/__init__.py.j2 +0 -0
  77. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/presets/unified/apps/core/models/base.py.j2 +0 -0
  78. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/presets/unified/apps/core/models/mixins.py.j2 +0 -0
  79. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/presets/unified/apps/core/utils/__init__.py.j2 +0 -0
  80. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/presets/unified/apps/core/utils/responses.py.j2 +0 -0
  81. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/project/ci/github_actions.j2 +0 -0
  82. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/project/ci/gitlab_ci.j2 +0 -0
  83. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/project/env_sample.j2 +0 -0
  84. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/project/gitignore.j2 +0 -0
  85. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/project/init.j2 +0 -0
  86. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/project/justfile.j2 +0 -0
  87. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/project/manage_py.j2 +0 -0
  88. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/project/procfile.j2 +0 -0
  89. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/project/pyproject_toml.j2 +0 -0
  90. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/project/readme.j2 +0 -0
  91. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/project/requirements.j2 +0 -0
  92. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/templates/project/runtime_txt.j2 +0 -0
  93. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/ui/__init__.py +0 -0
  94. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/utils/__init__.py +0 -0
  95. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/utils/django.py +0 -0
  96. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/utils/exceptions.py +0 -0
  97. {djinitx-0.3.0 → djinitx-0.3.2}/djinit/utils/validators.py +0 -0
  98. {djinitx-0.3.0 → djinitx-0.3.2}/ruff.toml +0 -0
@@ -11,9 +11,9 @@ jobs:
11
11
  permissions:
12
12
  id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
13
13
  steps:
14
- - uses: actions/checkout@v4
14
+ - uses: actions/checkout@v6
15
15
  - name: Set up Python
16
- uses: actions/setup-python@v4
16
+ uses: actions/setup-python@v6
17
17
  with:
18
18
  python-version: '3.12'
19
19
  cache: 'pip'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: djinitx
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: A CLI tool to setup Django project
5
5
  Project-URL: Homepage, https://github.com/S4NKALP/djinit
6
6
  Author-email: Sankalp Tharu <sankalptharu50028@gmail.com>
@@ -25,12 +25,10 @@ Classifier: Topic :: Internet :: WWW/HTTP
25
25
  Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
26
26
  Classifier: Topic :: Software Development :: Code Generators
27
27
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
28
- Requires-Python: >=3.13
29
- Requires-Dist: click>=8.3.0
28
+ Requires-Python: >=3.10
30
29
  Requires-Dist: jinja2>=3.1.0
31
- Requires-Dist: pre-commit>=4.5.0
30
+ Requires-Dist: questionary>=2.0.1
32
31
  Requires-Dist: rich>=14.2.0
33
- Requires-Dist: ruff>=0.14.2
34
32
  Description-Content-Type: text/markdown
35
33
 
36
34
  # djinit
@@ -10,5 +10,6 @@
10
10
  ## Enhancements
11
11
 
12
12
  - [ ] Add more project structure templates
13
- - [ ] Add interactive configuration wizard
13
+ - [x] Add interactive configuration wizard
14
14
  - [ ] Add testing framework setup (pytest, coverage)
15
+ - [x] Improve structure detection
@@ -9,7 +9,6 @@ from typing import Callable, List, Tuple
9
9
  from djinit.services.files import FileManager
10
10
  from djinit.services.project import ProjectManager
11
11
  from djinit.ui.console import UIFormatter
12
- from djinit.utils.security import generate_secret_command
13
12
 
14
13
 
15
14
  class Cli:
@@ -27,7 +26,8 @@ class Cli:
27
26
  self.project_manager = ProjectManager(project_dir, project_name, app_names, metadata)
28
27
  self.file_manager = FileManager(self.project_root, project_name, app_names, metadata)
29
28
 
30
- def run_setup(self) -> bool:
29
+ def _normalize_metadata(self) -> None:
30
+ """Normalize metadata and app names based on structure type."""
31
31
  if self.metadata.get("predefined_structure"):
32
32
  self.metadata.setdefault("project_module_name", "config")
33
33
  self.metadata.setdefault("nested_apps", True)
@@ -36,7 +36,7 @@ class Cli:
36
36
  self.app_names = ["users", "core"]
37
37
  self.project_manager.app_names = self.app_names
38
38
 
39
- if self.metadata.get("unified_structure"):
39
+ elif self.metadata.get("unified_structure"):
40
40
  self.metadata.setdefault("project_module_name", "core")
41
41
  self.metadata.setdefault("nested_apps", True)
42
42
  self.metadata.setdefault("nested_dir", "apps")
@@ -44,12 +44,15 @@ class Cli:
44
44
  self.app_names = []
45
45
  self.project_manager.app_names = self.app_names
46
46
 
47
- if self.metadata.get("single_structure"):
47
+ elif self.metadata.get("single_structure"):
48
48
  self.metadata.setdefault("nested_apps", False)
49
49
  if not self.app_names:
50
50
  self.app_names = []
51
51
  self.project_manager.app_names = self.app_names
52
52
 
53
+ def run_setup(self) -> bool:
54
+ self._normalize_metadata()
55
+
53
56
  steps: List[Tuple[str, Callable[[], None]]] = []
54
57
 
55
58
  steps.append(("Creating Django project", self.project_manager.create_project))
@@ -100,6 +103,7 @@ class Cli:
100
103
  self.file_manager.create_requirements,
101
104
  self.file_manager.create_readme,
102
105
  self.file_manager.create_env_file,
106
+ self.file_manager.create_djinit_config,
103
107
  lambda: self.file_manager.create_pyproject_toml(self.metadata),
104
108
  ]
105
109
 
@@ -114,7 +118,3 @@ class Cli:
114
118
 
115
119
  if self.metadata.get("use_gitlab_ci", True):
116
120
  self.file_manager.create_gitlab_ci()
117
-
118
- def generate_secret_keys(self) -> bool:
119
- generate_secret_command()
120
- return True
@@ -11,7 +11,7 @@ from rich.text import Text
11
11
 
12
12
  from djinit.services.app import AppManager
13
13
  from djinit.ui.console import UIColors, UIFormatter, console
14
- from djinit.utils.security import display_secret_keys, generate_multiple_keys
14
+ from djinit.utils.secretkey import display_secret_keys, generate_multiple_keys
15
15
  from djinit.utils.validators import validate_app_name
16
16
 
17
17
 
@@ -67,27 +67,17 @@ def handle_app_command(args: argparse.Namespace) -> None:
67
67
  UIFormatter.print_header("Django App Creation")
68
68
  UIFormatter.print_info("")
69
69
 
70
- tokens = args.app_name if isinstance(args.app_name, list) else [args.app_name]
71
- pieces = []
72
- for token in tokens:
73
- if token is None:
74
- continue
75
- for part in str(token).split(","):
76
- part = part.strip()
77
- if part:
78
- pieces.append(part)
79
- app_names = pieces
70
+ raw_tokens = args.app_name if isinstance(args.app_name, list) else [args.app_name]
71
+
72
+ # Flatten and clean tokens
73
+ app_names = [part.strip() for token in raw_tokens if token for part in str(token).split(",") if part.strip()]
80
74
 
81
75
  if not app_names:
82
76
  UIFormatter.print_error("App name cannot be empty")
83
77
  sys.exit(1)
84
78
 
85
- seen = set()
86
- deduped_names = []
87
- for name in app_names:
88
- if name not in seen:
89
- deduped_names.append(name)
90
- seen.add(name)
79
+ # Deduplicate while preserving order
80
+ deduped_names = list(dict.fromkeys(app_names))
91
81
 
92
82
  for name in deduped_names:
93
83
  is_valid, error_msg = validate_app_name(name)
@@ -16,6 +16,7 @@ from djinit.utils.common import (
16
16
  extract_existing_apps,
17
17
  find_project_dir,
18
18
  find_settings_path,
19
+ get_djinit_config,
19
20
  insert_apps_into_user_defined_apps,
20
21
  is_django_project,
21
22
  )
@@ -27,6 +28,7 @@ class AppManager:
27
28
  self.app_name = app_name
28
29
  self.current_dir = os.getcwd()
29
30
  self.manage_py_path = os.path.join(self.current_dir, "manage.py")
31
+ self.config = get_djinit_config(self.current_dir)
30
32
  self._project_structure_cache = None
31
33
 
32
34
  def create_app(self) -> bool:
@@ -126,36 +128,58 @@ class AppManager:
126
128
  def _get_project_structure(self) -> tuple[bool, Optional[str], str]:
127
129
  """Get project structure with caching to avoid repeated detection."""
128
130
  if self._project_structure_cache is None:
129
- project_dir, settings_base_path = find_project_dir(self.current_dir)
130
-
131
- if project_dir is None:
132
- self._project_structure_cache = (False, None, self.current_dir)
131
+ if self.config:
132
+ apps_config = self.config.get("apps", {})
133
+ nested = apps_config.get("nested", False)
134
+ nested_dir = apps_config.get("nested_dir")
135
+
136
+ # Determine base dir for apps
137
+ if nested and nested_dir:
138
+ apps_base_dir = os.path.join(self.current_dir, nested_dir)
139
+ else:
140
+ apps_base_dir = self.current_dir
141
+
142
+ self._project_structure_cache = (nested, nested_dir, apps_base_dir)
133
143
  else:
134
- self._project_structure_cache = detect_nested_structure_from_settings(
135
- settings_base_path, self.current_dir
136
- )
144
+ # Fallback to old detection method
145
+ project_dir, settings_base_path = find_project_dir(self.current_dir)
146
+
147
+ if project_dir is None:
148
+ self._project_structure_cache = (False, None, self.current_dir)
149
+ else:
150
+ self._project_structure_cache = detect_nested_structure_from_settings(
151
+ settings_base_path, self.current_dir
152
+ )
137
153
  return self._project_structure_cache
138
154
 
139
155
  def _is_predefined_structure(self) -> bool:
156
+ if self.config:
157
+ return self.config.get("structure", {}).get("predefined", False)
158
+
140
159
  apps_dir = os.path.join(self.current_dir, "apps")
141
160
  api_dir = os.path.join(self.current_dir, "api")
142
161
  return os.path.isdir(apps_dir) and os.path.isdir(api_dir)
143
162
 
144
163
  def _is_restricted_structure(self) -> bool:
145
164
  """Check if the project structure is Unified or Single Folder."""
146
- apps_api_dir = os.path.join(self.current_dir, "apps", "api")
147
- if os.path.isdir(apps_api_dir):
165
+ if self.config:
166
+ structure = self.config.get("structure", {})
167
+ return structure.get("unified", False) or structure.get("single", False)
168
+
169
+ # Check for Unified structure (apps/api exists)
170
+ if os.path.isdir(os.path.join(self.current_dir, "apps", "api")):
148
171
  return True
149
172
 
150
- apps_dir = os.path.join(self.current_dir, "apps")
151
- if os.path.isdir(apps_dir):
173
+ # Check for Predefined structure (apps/ exists but not apps/api)
174
+ if os.path.isdir(os.path.join(self.current_dir, "apps")):
152
175
  return False
153
176
 
177
+ # Check for Single Folder structure (models/ and api/ exist in project dir)
154
178
  project_dir, _ = find_project_dir(self.current_dir)
155
179
  if project_dir:
156
- models_dir = os.path.join(project_dir, "models")
157
- api_dir = os.path.join(project_dir, "api")
158
- if os.path.isdir(models_dir) and os.path.isdir(api_dir):
180
+ has_models = os.path.isdir(os.path.join(project_dir, "models"))
181
+ has_api = os.path.isdir(os.path.join(project_dir, "api"))
182
+ if has_models and has_api:
159
183
  return True
160
184
 
161
185
  return False
@@ -45,10 +45,12 @@ class FileManager:
45
45
  def _create_files_from_specs(self, base_dir: str, folder_specs: dict, base_context: dict = None) -> None:
46
46
  """Helper to create multiple folders and files from a specification dict."""
47
47
  base_context = base_context or {}
48
- for folder, files in folder_specs.items():
49
- folder_path = os.path.join(base_dir, folder)
48
+
49
+ for folder_name, file_templates in folder_specs.items():
50
+ folder_path = os.path.join(base_dir, folder_name)
50
51
  create_directory_with_init(folder_path, f"Created {folder_path}/__init__.py")
51
- for filename, template_path in files:
52
+
53
+ for filename, template_path in file_templates:
52
54
  file_path = os.path.join(folder_path, filename)
53
55
  create_file_from_template(file_path, template_path, base_context, f"Created {file_path}")
54
56
 
@@ -66,6 +68,52 @@ class FileManager:
66
68
  message = f"Created {prefix}{subdir}/__init__.py" if prefix else f"Created {subdir}/__init__.py"
67
69
  create_directory_with_init(subdir_path, message)
68
70
 
71
+ def _create_settings_package(self, settings_dir: str, base_context: dict, prefix: str) -> None:
72
+ """Helper to create settings package files (base, development, production)."""
73
+ from djinit.utils.secretkey import generate_secret_key
74
+
75
+ secret_key = generate_secret_key()
76
+ dev_context = {"secret_key": secret_key}
77
+
78
+ for filename, context in [
79
+ ("base.py", base_context),
80
+ ("development.py", dev_context),
81
+ ("production.py", base_context),
82
+ ]:
83
+ filepath = os.path.join(settings_dir, filename)
84
+ template_path = f"config/settings/{filename.replace('.py', '')}.j2"
85
+ self._render_and_create_file(
86
+ filepath,
87
+ template_path,
88
+ context,
89
+ f"Created {prefix}/settings/{filename}",
90
+ base_dir=os.path.dirname(
91
+ settings_dir
92
+ ), # Not strictly used by _render_and_create_file but good for context
93
+ should_format=True,
94
+ )
95
+
96
+ def _create_lifecycle_files(
97
+ self, target_dir: str, prefix: str, api_module: str = None, comment_out_api: bool = False
98
+ ) -> None:
99
+ """Helper to create urls.py, wsgi.py, and asgi.py."""
100
+
101
+ urls_context = {"api_module": api_module}
102
+ if comment_out_api:
103
+ urls_context["comment_out_api_url"] = True
104
+
105
+ files = [
106
+ ("urls.py", "config/urls/with_api.j2", urls_context),
107
+ ("wsgi.py", "config/wsgi.j2", {}),
108
+ ("asgi.py", "config/asgi.j2", {}),
109
+ ]
110
+
111
+ for filename, template, context in files:
112
+ filepath = os.path.join(target_dir, filename)
113
+ self._render_and_create_file(
114
+ filepath, template, context, f"Created {prefix}/{filename}", should_format=True
115
+ )
116
+
69
117
  def create_gitignore(self) -> None:
70
118
  self._render_and_create_file(".gitignore", "project/gitignore.j2", {}, "Created .gitignore file")
71
119
 
@@ -208,43 +256,17 @@ class FileManager:
208
256
  settings_dir = os.path.join(core_dir, "settings")
209
257
  create_directory_with_init(settings_dir, "Created core/settings/__init__.py")
210
258
 
211
- from djinit.utils.security import generate_secret_key
212
-
213
- secret_key = generate_secret_key()
214
-
215
- # In this new structure, 'apps' is the main app.
216
- # We assume the user wants 'apps' to be the app label.
217
- # Or perhaps 'apps' is just a container and the components inside are what matters?
218
- # Given the structure: apps/models, apps/views etc.
219
- # It seems 'apps' is acting as the app.
220
-
259
+ # Create settings files
221
260
  base_context = {
222
- "project_name": "core", # Project module is 'core'
223
- "app_names": ["apps"], # 'apps' is the main app
261
+ "project_name": "core",
262
+ "app_names": ["apps"],
224
263
  "use_database_url": self.metadata.get("use_database_url", True),
225
264
  "database_type": self.metadata.get("database_type", "postgresql"),
226
265
  }
227
- dev_context = {"secret_key": secret_key}
266
+ self._create_settings_package(settings_dir, base_context, "core")
228
267
 
229
- for filename, context in [
230
- ("base.py", base_context),
231
- ("development.py", dev_context),
232
- ("production.py", base_context),
233
- ]:
234
- filepath = os.path.join(settings_dir, filename)
235
- template_path = f"config/settings/{filename.replace('.py', '')}.j2"
236
- create_file_from_template(
237
- filepath, template_path, context, f"Created core/settings/{filename}", should_format=True
238
- )
239
-
240
- core_files = [
241
- ("urls.py", "config/urls/with_api.j2", {"api_module": "apps.api"}),
242
- ("wsgi.py", "config/wsgi.j2", {}),
243
- ("asgi.py", "config/asgi.j2", {}),
244
- ]
245
- for filename, template, context in core_files:
246
- filepath = os.path.join(core_dir, filename)
247
- create_file_from_template(filepath, template, context, f"Created core/{filename}", should_format=True)
268
+ # Create lifecycle files (urls, wsgi, asgi)
269
+ self._create_lifecycle_files(core_dir, "core", api_module="apps.api")
248
270
 
249
271
  # 2. Create 'apps' directory (The Main App)
250
272
  apps_dir = os.path.join(self.project_root, "apps")
@@ -287,43 +309,19 @@ class FileManager:
287
309
  settings_dir = os.path.join(project_dir, "settings")
288
310
  create_directory_with_init(settings_dir, f"Created {self.module_name}/settings/__init__.py")
289
311
 
290
- from djinit.utils.security import generate_secret_key
291
-
292
- secret_key = generate_secret_key()
293
-
312
+ # Create settings files
294
313
  base_context = {
295
314
  "project_name": self.module_name,
296
- "app_names": [self.module_name], # Project acts as the app
315
+ "app_names": [self.module_name],
297
316
  "use_database_url": self.metadata.get("use_database_url", True),
298
317
  "database_type": self.metadata.get("database_type", "postgresql"),
299
318
  }
300
- dev_context = {"secret_key": secret_key}
301
-
302
- for filename, context in [
303
- ("base.py", base_context),
304
- ("development.py", dev_context),
305
- ("production.py", base_context),
306
- ]:
307
- filepath = os.path.join(settings_dir, filename)
308
- template_path = f"config/settings/{filename.replace('.py', '')}.j2"
309
- create_file_from_template(
310
- filepath, template_path, context, f"Created {self.module_name}/settings/{filename}", should_format=True
311
- )
319
+ self._create_settings_package(settings_dir, base_context, self.module_name)
312
320
 
313
- project_files = [
314
- (
315
- "urls.py",
316
- "config/urls/with_api.j2",
317
- {"api_module": f"{self.module_name}.api", "comment_out_api_url": True},
318
- ),
319
- ("wsgi.py", "config/wsgi.j2", {}),
320
- ("asgi.py", "config/asgi.j2", {}),
321
- ]
322
- for filename, template, context in project_files:
323
- filepath = os.path.join(project_dir, filename)
324
- create_file_from_template(
325
- filepath, template, context, f"Created {self.module_name}/{filename}", should_format=True
326
- )
321
+ # Create lifecycle files (urls, wsgi, asgi)
322
+ self._create_lifecycle_files(
323
+ project_dir, self.module_name, api_module=f"{self.module_name}.api", comment_out_api=True
324
+ )
327
325
 
328
326
  components = ["admin", "api", "models", "tests"]
329
327
  self._create_subdirectories_with_init(project_dir, components, f"{self.module_name}/")
@@ -395,3 +393,37 @@ class FileManager:
395
393
  context,
396
394
  "Created GitLab CI configuration (.gitlab-ci.yml)",
397
395
  )
396
+
397
+ def create_djinit_config(self) -> None:
398
+ """Create .djinit configuration file."""
399
+ import json
400
+
401
+ is_predefined = self.metadata.get("predefined_structure", False)
402
+ is_unified = self.metadata.get("unified_structure", False)
403
+ is_single = self.metadata.get("single_structure", False)
404
+ is_standard = not (is_predefined or is_unified or is_single)
405
+
406
+ config = {
407
+ "project_name": self.project_name,
408
+ "structure": {
409
+ "standard": is_standard,
410
+ "predefined": is_predefined,
411
+ "unified": is_unified,
412
+ "single": is_single,
413
+ },
414
+ "apps": {
415
+ "nested": self.metadata.get("nested_apps", False),
416
+ "nested_dir": self.metadata.get("nested_dir"),
417
+ },
418
+ "settings": {
419
+ "use_database_url": self.metadata.get("use_database_url", True),
420
+ "database_type": self.metadata.get("database_type", "postgresql"),
421
+ },
422
+ "cicd": {
423
+ "github": self.metadata.get("use_github_actions", False),
424
+ "gitlab": self.metadata.get("use_gitlab_ci", False),
425
+ },
426
+ }
427
+
428
+ filepath = os.path.join(self.project_root, ".djinit")
429
+ create_file_with_content(filepath, json.dumps(config, indent=4), "Created .djinit configuration file")
@@ -89,23 +89,25 @@ class ProjectManager:
89
89
  UIFormatter.print_success(f"Added apps to USER_DEFINED_APPS: {added_apps_str}")
90
90
 
91
91
  def validate_project_structure(self) -> None:
92
+ join = lambda *args: os.path.join(self.project_root, *args)
93
+
92
94
  required_files = [
93
- os.path.join(self.project_root, "manage.py"),
94
- os.path.join(self.project_root, self.module_name, "__init__.py"),
95
- os.path.join(self.project_root, self.module_name, "settings", "__init__.py"),
96
- os.path.join(self.project_root, self.module_name, "settings", "base.py"),
97
- os.path.join(self.project_root, self.module_name, "settings", "development.py"),
98
- os.path.join(self.project_root, self.module_name, "settings", "production.py"),
99
- os.path.join(self.project_root, self.module_name, "urls.py"),
100
- os.path.join(self.project_root, self.module_name, "wsgi.py"),
101
- os.path.join(self.project_root, self.module_name, "asgi.py"),
95
+ join("manage.py"),
96
+ join(self.module_name, "__init__.py"),
97
+ join(self.module_name, "settings", "__init__.py"),
98
+ join(self.module_name, "settings", "base.py"),
99
+ join(self.module_name, "settings", "development.py"),
100
+ join(self.module_name, "settings", "production.py"),
101
+ join(self.module_name, "urls.py"),
102
+ join(self.module_name, "wsgi.py"),
103
+ join(self.module_name, "asgi.py"),
102
104
  ]
103
105
 
104
106
  apps_base_dir = self._get_apps_base_dir()
105
107
 
106
108
  if self.metadata.get("unified_structure"):
107
109
  # Validate Unified Structure
108
- apps_dir = os.path.join(self.project_root, "apps")
110
+ apps_dir = join("apps")
109
111
  unified_files = [
110
112
  os.path.join(apps_dir, "__init__.py"),
111
113
  os.path.join(apps_dir, "apps.py"),
@@ -124,16 +126,17 @@ class ProjectManager:
124
126
 
125
127
  elif not self.metadata.get("predefined_structure") and not self.metadata.get("single_structure"):
126
128
  for app_name in self.app_names:
129
+ app_path = lambda f, app=app_name: os.path.join(apps_base_dir, app, f)
127
130
  app_files = [
128
- os.path.join(apps_base_dir, app_name, "__init__.py"),
129
- os.path.join(apps_base_dir, app_name, "apps.py"),
130
- os.path.join(apps_base_dir, app_name, "models.py"),
131
- os.path.join(apps_base_dir, app_name, "views.py"),
132
- os.path.join(apps_base_dir, app_name, "serializers.py"),
133
- os.path.join(apps_base_dir, app_name, "routes.py"),
134
- os.path.join(apps_base_dir, app_name, "tests.py"),
135
- os.path.join(apps_base_dir, app_name, "migrations"),
136
- os.path.join(apps_base_dir, app_name, "admin.py"),
131
+ app_path("__init__.py"),
132
+ app_path("apps.py"),
133
+ app_path("models.py"),
134
+ app_path("views.py"),
135
+ app_path("serializers.py"),
136
+ app_path("routes.py"),
137
+ app_path("tests.py"),
138
+ app_path("migrations"),
139
+ app_path("admin.py"),
137
140
  ]
138
141
  required_files.extend(app_files)
139
142
 
@@ -6,6 +6,7 @@ Provides clean, minimal styling and user interface components.
6
6
  from contextlib import contextmanager
7
7
  from typing import Any, Dict, List, Optional
8
8
 
9
+ import questionary
9
10
  from rich import box
10
11
  from rich.console import Console
11
12
  from rich.panel import Panel
@@ -164,20 +165,28 @@ class UIFormatter:
164
165
 
165
166
  @staticmethod
166
167
  def confirm(prompt: str, default: bool = True) -> bool:
167
- """Display a confirmation prompt"""
168
- default_str = "Y/n" if default else "y/N"
169
- response = console.input(f"[bold cyan]?[/bold cyan] {prompt} [{default_str}]: ").strip().lower()
170
- if not response:
171
- return default
172
- return response in ("y", "yes")
168
+ """Display a confirmation prompt as a selectable list"""
169
+ choices = [
170
+ questionary.Choice("Yes", value=True),
171
+ questionary.Choice("No", value=False),
172
+ ]
173
+ default_choice = choices[0] if default else choices[1]
174
+
175
+ result = questionary.select(
176
+ prompt,
177
+ choices=choices,
178
+ default=default_choice,
179
+ ).ask()
180
+
181
+ if result is None:
182
+ raise KeyboardInterrupt
183
+
184
+ return result
173
185
 
174
186
  @staticmethod
175
187
  def prompt(message: str, default: Optional[str] = None) -> str:
176
188
  """Display an input prompt"""
177
- if default:
178
- message = f"{message} [{default}]"
179
- response = console.input(f"[bold cyan]?[/bold cyan] {message}: ").strip()
180
- return response or default or ""
189
+ return questionary.text(message, default=default or "").ask()
181
190
 
182
191
  @staticmethod
183
192
  def handle_exception(e: Exception):