ntc-dev-kit 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ntc/__init__.py +5 -0
- ntc/app_creator.py +296 -0
- ntc/cli.py +4 -0
- ntc/cli_enhanced.py +820 -0
- ntc/composer/__init__.py +0 -0
- ntc/composer/main.py +443 -0
- ntc/deployment/__init__.py +0 -0
- ntc/templates/__init__.py +2 -0
- ntc/templates/auth_app/__init__.py +0 -0
- ntc/templates/auth_app/admin.py +24 -0
- ntc/templates/auth_app/apps.py +6 -0
- ntc/templates/auth_app/models.py +30 -0
- ntc/templates/auth_app/serializers.py +101 -0
- ntc/templates/auth_app/urls.py +19 -0
- ntc/templates/auth_app/views.py +175 -0
- ntc/templates/drf_structure/__init__.py +1 -0
- ntc/templates/drf_structure/drf_structure.md +83 -0
- ntc/templates/drf_structure/main.py +389 -0
- ntc/templates/drf_structure/structure.json +126 -0
- ntc/templates/drf_structure/templates/.entry/start_core_backend.sh +37 -0
- ntc/templates/drf_structure/templates/.entry/start_tasks_backend.sh +30 -0
- ntc/templates/drf_structure/templates/CLAUDE.md +194 -0
- ntc/templates/drf_structure/templates/README.md +117 -0
- ntc/templates/drf_structure/templates/apps/app_admin.py +3 -0
- ntc/templates/drf_structure/templates/apps/app_apps.py +6 -0
- ntc/templates/drf_structure/templates/apps/app_models.py +4 -0
- ntc/templates/drf_structure/templates/apps/app_urls.py +8 -0
- ntc/templates/drf_structure/templates/apps/app_views.py +6 -0
- ntc/templates/drf_structure/templates/apps/common_apps.py +6 -0
- ntc/templates/drf_structure/templates/apps/common_models.py +8 -0
- ntc/templates/drf_structure/templates/config/gunicorn_config.py +9 -0
- ntc/templates/drf_structure/templates/config/http.conf +33 -0
- ntc/templates/drf_structure/templates/core/base_model.py +9 -0
- ntc/templates/drf_structure/templates/core/formatter.py +5 -0
- ntc/templates/drf_structure/templates/core/logger.py +8 -0
- ntc/templates/drf_structure/templates/core/pagination.py +6 -0
- ntc/templates/drf_structure/templates/core/response_wrapper.py +7 -0
- ntc/templates/drf_structure/templates/core/services/azure/email/backend.py +108 -0
- ntc/templates/drf_structure/templates/core/services/azure/email/client.py +296 -0
- ntc/templates/drf_structure/templates/core/services/azure/email/notifier.py +102 -0
- ntc/templates/drf_structure/templates/core/services/object_storage.py +285 -0
- ntc/templates/drf_structure/templates/django_project/asgi.py +5 -0
- ntc/templates/drf_structure/templates/django_project/celery.py +8 -0
- ntc/templates/drf_structure/templates/django_project/dev_settings.py +2 -0
- ntc/templates/drf_structure/templates/django_project/django_settings.txt +322 -0
- ntc/templates/drf_structure/templates/django_project/management_command.py +114 -0
- ntc/templates/drf_structure/templates/django_project/prod_settings.py +3 -0
- ntc/templates/drf_structure/templates/django_project/schema.py +7 -0
- ntc/templates/drf_structure/templates/django_project/urls.py +6 -0
- ntc/templates/drf_structure/templates/django_project/wsgi.py +5 -0
- ntc/templates/drf_structure/templates/docker/docker-compose.yml +11 -0
- ntc/templates/drf_structure/templates/knowledge/knowledge_description.md +201 -0
- ntc/templates/drf_structure/templates/knowledge/knowledge_design_pattern.md +722 -0
- ntc/templates/drf_structure/templates/knowledge/knowledge_structure.md +151 -0
- ntc/templates/drf_structure/templates/root/README.md +307 -0
- ntc/templates/drf_structure/templates/root/django_env.txt +98 -0
- ntc/templates/drf_structure/templates/root/django_requirements.txt +24 -0
- ntc/templates/drf_structure/templates/root/gitignore.txt +6 -0
- ntc/templates/drf_structure/templates/root/manage.py +11 -0
- ntc/templates/flutter/.gitignore +44 -0
- ntc/templates/flutter/.knowledge/knowledge_description.md +215 -0
- ntc/templates/flutter/.knowledge/knowledge_design_pattern.md +310 -0
- ntc/templates/flutter/.knowledge/knowledge_structure.md +83 -0
- ntc/templates/flutter/CLAUDE.md +190 -0
- ntc/templates/flutter/__init__.py +0 -0
- ntc/templates/flutter/analysis_options.yaml +9 -0
- ntc/templates/flutter/main.py +187 -0
- ntc/templates/flutter/pubspec.yaml +65 -0
- ntc/templates/flutter/structure.json +51 -0
- ntc/templates/nextjs/.env.example +10 -0
- ntc/templates/nextjs/.gitignore +36 -0
- ntc/templates/nextjs/CLAUDE.md +181 -0
- ntc/templates/nextjs/README.md +94 -0
- ntc/templates/nextjs/__init__.py +0 -0
- ntc/templates/nextjs/knowledge/knowledge_description.md +208 -0
- ntc/templates/nextjs/knowledge/knowledge_design_pattern.md +460 -0
- ntc/templates/nextjs/knowledge/knowledge_structure.md +75 -0
- ntc/templates/nextjs/main.py +224 -0
- ntc/templates/nextjs/package.json +25 -0
- ntc/templates/nextjs/src/app/globals.css +26 -0
- ntc/templates/nextjs/src/app/layout.tsx +59 -0
- ntc/templates/nextjs/src/app/page.tsx +21 -0
- ntc/templates/nextjs/src/components/Header.tsx +16 -0
- ntc/templates/nextjs/src/services/api.ts +81 -0
- ntc/templates/nextjs/src/utils/constants.ts +1 -0
- ntc/templates/nextjs/structure.json +32 -0
- ntc/templates/nextjs/tsconfig.json +27 -0
- ntc/templates/react_admin/.env.example +1 -0
- ntc/templates/react_admin/.gitignore +10 -0
- ntc/templates/react_admin/CLAUDE.md +210 -0
- ntc/templates/react_admin/README.md +72 -0
- ntc/templates/react_admin/__init__.py +0 -0
- ntc/templates/react_admin/index.html +13 -0
- ntc/templates/react_admin/knowledge/knowledge_description.md +202 -0
- ntc/templates/react_admin/knowledge/knowledge_design_pattern.md +448 -0
- ntc/templates/react_admin/knowledge/knowledge_structure.md +97 -0
- ntc/templates/react_admin/main.py +208 -0
- ntc/templates/react_admin/package.json +35 -0
- ntc/templates/react_admin/src/App.tsx +60 -0
- ntc/templates/react_admin/src/components/ProtectedRoute.tsx +16 -0
- ntc/templates/react_admin/src/components/Sidebar.tsx +90 -0
- ntc/templates/react_admin/src/components/TopNav.tsx +44 -0
- ntc/templates/react_admin/src/context/AuthContext.tsx +107 -0
- ntc/templates/react_admin/src/context/ThemeContext.tsx +34 -0
- ntc/templates/react_admin/src/context/WorkspaceContext.tsx +28 -0
- ntc/templates/react_admin/src/hooks/useApi.ts +39 -0
- ntc/templates/react_admin/src/index.css +34 -0
- ntc/templates/react_admin/src/layouts/AuthLayout.tsx +23 -0
- ntc/templates/react_admin/src/layouts/DashboardLayout.tsx +24 -0
- ntc/templates/react_admin/src/main.tsx +5 -0
- ntc/templates/react_admin/src/pages/DashboardPage.tsx +10 -0
- ntc/templates/react_admin/src/pages/auth/LoginPage.tsx +79 -0
- ntc/templates/react_admin/src/pages/auth/RegisterPage.tsx +93 -0
- ntc/templates/react_admin/src/services/api.service.ts +44 -0
- ntc/templates/react_admin/src/services/base.service.ts +193 -0
- ntc/templates/react_admin/structure.json +52 -0
- ntc/templates/react_admin/tsconfig.app.json +25 -0
- ntc/templates/react_admin/tsconfig.json +7 -0
- ntc/templates/react_admin/tsconfig.node.json +19 -0
- ntc/templates/react_admin/vite.config.ts +13 -0
- ntc/templates/react_structure/CLAUDE.md +176 -0
- ntc/templates/react_structure/__init__.py +1 -0
- ntc/templates/react_structure/index.css +21 -0
- ntc/templates/react_structure/index.html +13 -0
- ntc/templates/react_structure/knowledge/knowledge_description.md +170 -0
- ntc/templates/react_structure/knowledge/knowledge_design_pattern.md +411 -0
- ntc/templates/react_structure/knowledge/knowledge_structure.md +74 -0
- ntc/templates/react_structure/main.py +217 -0
- ntc/templates/react_structure/package.json +32 -0
- ntc/templates/react_structure/src/App.tsx +63 -0
- ntc/templates/react_structure/src/components/Header.tsx +51 -0
- ntc/templates/react_structure/src/main.tsx +5 -0
- ntc/templates/react_structure/src/services/api.ts +90 -0
- ntc/templates/react_structure/src/utils/constants.ts +1 -0
- ntc/templates/react_structure/structure.json +65 -0
- ntc/templates/react_structure/tsconfig.app.json +25 -0
- ntc/templates/react_structure/tsconfig.json +7 -0
- ntc/templates/react_structure/tsconfig.node.json +19 -0
- ntc/templates/react_structure/vite.config.ts +12 -0
- ntc/utils/__init__.py +1 -0
- ntc/utils/environment.py +67 -0
- ntc/utils/output.py +41 -0
- ntc_dev_kit-1.0.0.dist-info/METADATA +188 -0
- ntc_dev_kit-1.0.0.dist-info/RECORD +148 -0
- ntc_dev_kit-1.0.0.dist-info/WHEEL +5 -0
- ntc_dev_kit-1.0.0.dist-info/entry_points.txt +2 -0
- ntc_dev_kit-1.0.0.dist-info/licenses/LICENSE +21 -0
- ntc_dev_kit-1.0.0.dist-info/top_level.txt +1 -0
ntc/composer/__init__.py
ADDED
|
File without changes
|
ntc/composer/main.py
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import yaml
|
|
4
|
+
import msvcrt
|
|
5
|
+
import shutil
|
|
6
|
+
from colorama import Fore, Style
|
|
7
|
+
from ntc.utils.output import print_success, print_error, print_warning, print_info
|
|
8
|
+
|
|
9
|
+
def is_directory_empty(directory):
|
|
10
|
+
"""Check if a directory is empty (ignoring hidden files)."""
|
|
11
|
+
if not os.path.exists(directory):
|
|
12
|
+
return True
|
|
13
|
+
|
|
14
|
+
items = [item for item in os.listdir(directory) if not item.startswith('.')]
|
|
15
|
+
return len(items) == 0
|
|
16
|
+
|
|
17
|
+
def get_project_choices():
|
|
18
|
+
"""Get user choices for which projects to create using interactive menu."""
|
|
19
|
+
options = [
|
|
20
|
+
{"label": "Backend (Django REST Framework)", "value": "backend", "selected": False},
|
|
21
|
+
{"label": "Website (Next.js)", "value": "website", "selected": False},
|
|
22
|
+
{"label": "Mobile App (Flutter)", "value": "mobile", "selected": False},
|
|
23
|
+
{"label": "Admin Dashboard (React Admin)", "value": "admin", "selected": False},
|
|
24
|
+
{"label": "All of the above", "value": "all", "selected": False},
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
current_index = 0
|
|
28
|
+
|
|
29
|
+
while True:
|
|
30
|
+
# Clear screen and redraw menu
|
|
31
|
+
os.system('cls' if os.name == 'nt' else 'clear')
|
|
32
|
+
print_info("Select the projects you want to create:")
|
|
33
|
+
print("─" * 50)
|
|
34
|
+
print("Use ↑↓ arrows to navigate, SPACE to select/deselect, ENTER to confirm")
|
|
35
|
+
print("─" * 50)
|
|
36
|
+
|
|
37
|
+
for i, option in enumerate(options):
|
|
38
|
+
prefix = "→ " if i == current_index else " "
|
|
39
|
+
checkmark = "✓" if option["selected"] else " "
|
|
40
|
+
label = option["label"]
|
|
41
|
+
|
|
42
|
+
if i == current_index:
|
|
43
|
+
print(f"{Fore.GREEN}{prefix}[{checkmark}] {label}{Style.RESET_ALL}")
|
|
44
|
+
else:
|
|
45
|
+
print(f"{prefix}[{checkmark}] {label}")
|
|
46
|
+
|
|
47
|
+
# Get key press
|
|
48
|
+
key = msvcrt.getch()
|
|
49
|
+
|
|
50
|
+
if key == b'\r': # Enter key
|
|
51
|
+
selected_projects = []
|
|
52
|
+
for option in options:
|
|
53
|
+
if option["selected"]:
|
|
54
|
+
if option["value"] == "all":
|
|
55
|
+
selected_projects = ["backend", "website", "mobile", "admin"]
|
|
56
|
+
break
|
|
57
|
+
else:
|
|
58
|
+
selected_projects.append(option["value"])
|
|
59
|
+
|
|
60
|
+
if not selected_projects:
|
|
61
|
+
print_warning("No projects selected. Defaulting to backend only.")
|
|
62
|
+
return ["backend"]
|
|
63
|
+
|
|
64
|
+
return selected_projects
|
|
65
|
+
|
|
66
|
+
elif key == b' ': # Space bar
|
|
67
|
+
if options[current_index]["value"] == "all":
|
|
68
|
+
# Toggle "All" option
|
|
69
|
+
options[current_index]["selected"] = not options[current_index]["selected"]
|
|
70
|
+
if options[current_index]["selected"]:
|
|
71
|
+
# Select all other options
|
|
72
|
+
for i in range(len(options) - 1):
|
|
73
|
+
options[i]["selected"] = True
|
|
74
|
+
else:
|
|
75
|
+
# Deselect all other options
|
|
76
|
+
for i in range(len(options) - 1):
|
|
77
|
+
options[i]["selected"] = False
|
|
78
|
+
else:
|
|
79
|
+
# Toggle individual option
|
|
80
|
+
options[current_index]["selected"] = not options[current_index]["selected"]
|
|
81
|
+
# If any individual option is deselected, deselect "All"
|
|
82
|
+
if not options[current_index]["selected"]:
|
|
83
|
+
options[-1]["selected"] = False
|
|
84
|
+
# If all individual options are selected, select "All"
|
|
85
|
+
elif all(options[i]["selected"] for i in range(len(options) - 1)):
|
|
86
|
+
options[-1]["selected"] = True
|
|
87
|
+
|
|
88
|
+
elif key == b'\x00' or key == b'\xe0': # Arrow key prefix
|
|
89
|
+
key = msvcrt.getch()
|
|
90
|
+
if key == b'H': # Up arrow
|
|
91
|
+
current_index = (current_index - 1) % len(options)
|
|
92
|
+
elif key == b'P': # Down arrow
|
|
93
|
+
current_index = (current_index + 1) % len(options)
|
|
94
|
+
|
|
95
|
+
def create_ntc_yml(project_name, projects, root_dir):
|
|
96
|
+
"""Create ntc.yml configuration file."""
|
|
97
|
+
config = {
|
|
98
|
+
"project": {
|
|
99
|
+
"name": project_name,
|
|
100
|
+
"version": "1.0.0",
|
|
101
|
+
"description": f"{project_name} - Full stack application"
|
|
102
|
+
},
|
|
103
|
+
"components": {},
|
|
104
|
+
"deployment": {
|
|
105
|
+
"platform": "caprover",
|
|
106
|
+
"environment": "production"
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# Add component configurations
|
|
111
|
+
if "backend" in projects:
|
|
112
|
+
config["components"]["backend"] = {
|
|
113
|
+
"type": "django",
|
|
114
|
+
"path": f"{project_name}_backend",
|
|
115
|
+
"port": 8000,
|
|
116
|
+
"environment": {
|
|
117
|
+
"type": "venv", # Default to venv, will be updated by user
|
|
118
|
+
"path": f"{project_name}_backend/env"
|
|
119
|
+
},
|
|
120
|
+
"dependencies": {
|
|
121
|
+
"file": "requirements.txt",
|
|
122
|
+
"manager": "pip"
|
|
123
|
+
},
|
|
124
|
+
"start_command": "python manage.py runserver"
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if "website" in projects:
|
|
128
|
+
config["components"]["website"] = {
|
|
129
|
+
"type": "nextjs",
|
|
130
|
+
"path": f"{project_name}_website",
|
|
131
|
+
"port": 3000,
|
|
132
|
+
"environment": {
|
|
133
|
+
"type": "node",
|
|
134
|
+
"path": None
|
|
135
|
+
},
|
|
136
|
+
"dependencies": {
|
|
137
|
+
"file": "package.json",
|
|
138
|
+
"manager": "npm"
|
|
139
|
+
},
|
|
140
|
+
"start_command": "npm run dev"
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if "mobile" in projects:
|
|
144
|
+
config["components"]["mobile"] = {
|
|
145
|
+
"type": "flutter",
|
|
146
|
+
"path": f"{project_name}_mobile",
|
|
147
|
+
"port": None,
|
|
148
|
+
"environment": {
|
|
149
|
+
"type": "flutter",
|
|
150
|
+
"path": None
|
|
151
|
+
},
|
|
152
|
+
"dependencies": {
|
|
153
|
+
"file": "pubspec.yaml",
|
|
154
|
+
"manager": "flutter"
|
|
155
|
+
},
|
|
156
|
+
"start_command": "flutter run"
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if "admin" in projects:
|
|
160
|
+
config["components"]["admin"] = {
|
|
161
|
+
"type": "react-admin",
|
|
162
|
+
"path": f"{project_name}_admin",
|
|
163
|
+
"port": 5173,
|
|
164
|
+
"environment": {
|
|
165
|
+
"type": "node",
|
|
166
|
+
"path": None
|
|
167
|
+
},
|
|
168
|
+
"dependencies": {
|
|
169
|
+
"file": "package.json",
|
|
170
|
+
"manager": "npm"
|
|
171
|
+
},
|
|
172
|
+
"start_command": "npm run dev"
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
# Write ntc.yml
|
|
176
|
+
ntc_yml_path = os.path.join(root_dir, "ntc.yml")
|
|
177
|
+
with open(ntc_yml_path, "w") as f:
|
|
178
|
+
yaml.dump(config, f, default_flow_style=False, sort_keys=False)
|
|
179
|
+
|
|
180
|
+
print_success(f"Created ntc.yml configuration at: {ntc_yml_path}")
|
|
181
|
+
|
|
182
|
+
def create_project(project_type, project_name, root_dir):
|
|
183
|
+
"""Create a single project using the appropriate template."""
|
|
184
|
+
from ntc.templates.drf_structure.main import init_drf_project
|
|
185
|
+
from ntc.templates.nextjs.main import init_nextjs_project
|
|
186
|
+
from ntc.templates.flutter.main import init_flutter_project
|
|
187
|
+
from ntc.templates.react_admin.main import init_react_admin_project
|
|
188
|
+
|
|
189
|
+
project_postfix_map = {
|
|
190
|
+
"backend": "_backend",
|
|
191
|
+
"website": "_website",
|
|
192
|
+
"mobile": "_mobile",
|
|
193
|
+
"admin": "_admin"
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
project_postfix = project_postfix_map[project_type]
|
|
197
|
+
full_project_name = f"{project_name}{project_postfix}"
|
|
198
|
+
project_path = os.path.join(root_dir, full_project_name)
|
|
199
|
+
|
|
200
|
+
print_info(f"Creating {project_type} project: {full_project_name}")
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
if project_type == "backend":
|
|
204
|
+
init_drf_project(root_dir, full_project_name)
|
|
205
|
+
elif project_type == "website":
|
|
206
|
+
init_nextjs_project(root_dir, full_project_name)
|
|
207
|
+
elif project_type == "mobile":
|
|
208
|
+
init_flutter_project(root_dir, full_project_name)
|
|
209
|
+
elif project_type == "admin":
|
|
210
|
+
init_react_admin_project(root_dir, full_project_name)
|
|
211
|
+
|
|
212
|
+
print_success(f"Successfully created {project_type} project: {full_project_name}")
|
|
213
|
+
return True
|
|
214
|
+
except Exception as e:
|
|
215
|
+
print_error(f"Failed to create {project_type} project: {e}")
|
|
216
|
+
return False
|
|
217
|
+
|
|
218
|
+
def compose_project(project_name, root_dir=None):
|
|
219
|
+
"""Compose a full-stack project with multiple components."""
|
|
220
|
+
if root_dir is None:
|
|
221
|
+
root_dir = os.getcwd()
|
|
222
|
+
|
|
223
|
+
# Check if directory is empty
|
|
224
|
+
if not is_directory_empty(root_dir):
|
|
225
|
+
print_error("Directory is not empty. The composer requires an empty directory.")
|
|
226
|
+
print_info("Please run this command in an empty directory.")
|
|
227
|
+
sys.exit(1)
|
|
228
|
+
|
|
229
|
+
print_info(f"🎵 Composing project: {project_name}")
|
|
230
|
+
print_info(f"Root directory: {root_dir}")
|
|
231
|
+
print()
|
|
232
|
+
|
|
233
|
+
# Get project choices
|
|
234
|
+
projects = get_project_choices()
|
|
235
|
+
|
|
236
|
+
if not projects:
|
|
237
|
+
print_error("No projects selected. Exiting.")
|
|
238
|
+
sys.exit(1)
|
|
239
|
+
|
|
240
|
+
# Clear screen after project selection
|
|
241
|
+
os.system('cls' if os.name == 'nt' else 'clear')
|
|
242
|
+
print_info(f"Selected projects: {', '.join(projects)}")
|
|
243
|
+
print()
|
|
244
|
+
|
|
245
|
+
# Create each project
|
|
246
|
+
successful_projects = []
|
|
247
|
+
failed_projects = []
|
|
248
|
+
|
|
249
|
+
for project_type in projects:
|
|
250
|
+
if create_project(project_type, project_name, root_dir):
|
|
251
|
+
successful_projects.append(project_type)
|
|
252
|
+
else:
|
|
253
|
+
failed_projects.append(project_type)
|
|
254
|
+
|
|
255
|
+
# Create ntc.yml configuration
|
|
256
|
+
if successful_projects:
|
|
257
|
+
create_ntc_yml(project_name, successful_projects, root_dir)
|
|
258
|
+
|
|
259
|
+
# Clear screen before showing summary
|
|
260
|
+
os.system('cls' if os.name == 'nt' else 'clear')
|
|
261
|
+
|
|
262
|
+
# Summary
|
|
263
|
+
print("─" * 50)
|
|
264
|
+
print(f"{Fore.GREEN}COMPOSER SUMMARY{Style.RESET_ALL}")
|
|
265
|
+
print("─" * 50)
|
|
266
|
+
|
|
267
|
+
if successful_projects:
|
|
268
|
+
print_success(f"Successfully created: {', '.join(successful_projects)}")
|
|
269
|
+
if failed_projects:
|
|
270
|
+
print_error(f"Failed to create: {', '.join(failed_projects)}")
|
|
271
|
+
|
|
272
|
+
print()
|
|
273
|
+
print_info("Project structure:")
|
|
274
|
+
for project_type in successful_projects:
|
|
275
|
+
project_postfix_map = {
|
|
276
|
+
"backend": "_backend",
|
|
277
|
+
"website": "_website",
|
|
278
|
+
"mobile": "_mobile",
|
|
279
|
+
"admin": "_admin"
|
|
280
|
+
}
|
|
281
|
+
postfix = project_postfix_map[project_type]
|
|
282
|
+
print(f" 📁 {project_name}{postfix}")
|
|
283
|
+
|
|
284
|
+
print()
|
|
285
|
+
print_info(f"Configuration file: ntc.yml")
|
|
286
|
+
print_info("Use this file to configure deployment and project settings.")
|
|
287
|
+
print()
|
|
288
|
+
print_success("🎉 Project composition complete!")
|
|
289
|
+
|
|
290
|
+
def compose_project_in_current_dir(project_name):
|
|
291
|
+
"""Compose a full-stack project in the current directory."""
|
|
292
|
+
compose_project(project_name, os.getcwd())
|
|
293
|
+
|
|
294
|
+
def start_services(root_dir=None):
|
|
295
|
+
"""Start all services defined in ntc.yml configuration in terminal splits."""
|
|
296
|
+
if root_dir is None:
|
|
297
|
+
root_dir = os.getcwd()
|
|
298
|
+
|
|
299
|
+
ntc_yml_path = os.path.join(root_dir, "ntc.yml")
|
|
300
|
+
|
|
301
|
+
if not os.path.exists(ntc_yml_path):
|
|
302
|
+
print_error("ntc.yml not found. Are you in a project root directory?")
|
|
303
|
+
return
|
|
304
|
+
|
|
305
|
+
with open(ntc_yml_path, "r") as f:
|
|
306
|
+
config = yaml.safe_load(f)
|
|
307
|
+
|
|
308
|
+
components = config.get("components", {})
|
|
309
|
+
project_name = config.get("project", {}).get("name", "project")
|
|
310
|
+
|
|
311
|
+
if not components:
|
|
312
|
+
print_warning("No components found in ntc.yml configuration.")
|
|
313
|
+
return
|
|
314
|
+
|
|
315
|
+
print_info(f"Starting services for {project_name}...")
|
|
316
|
+
print()
|
|
317
|
+
|
|
318
|
+
# Check if Windows Terminal or tmux is available
|
|
319
|
+
wt_available = shutil.which("wt") is not None
|
|
320
|
+
tmux_available = shutil.which("tmux") is not None
|
|
321
|
+
|
|
322
|
+
# Prepare commands with environment activation and dependency installation
|
|
323
|
+
prepared_commands = []
|
|
324
|
+
|
|
325
|
+
for component_name, component_config in components.items():
|
|
326
|
+
component_type = component_config.get("type")
|
|
327
|
+
component_path = os.path.join(root_dir, component_config.get("path"))
|
|
328
|
+
|
|
329
|
+
if not os.path.exists(component_path):
|
|
330
|
+
print_warning(f"Component path not found: {component_path}")
|
|
331
|
+
continue
|
|
332
|
+
|
|
333
|
+
print_info(f"Preparing {component_name} ({component_type})...")
|
|
334
|
+
|
|
335
|
+
# Build command with environment activation
|
|
336
|
+
env_config = component_config.get("environment", {})
|
|
337
|
+
env_type = env_config.get("type", "none")
|
|
338
|
+
env_path = env_config.get("path")
|
|
339
|
+
|
|
340
|
+
# Check and install dependencies
|
|
341
|
+
dep_config = component_config.get("dependencies", {})
|
|
342
|
+
dep_file = dep_config.get("file")
|
|
343
|
+
dep_manager = dep_config.get("manager")
|
|
344
|
+
|
|
345
|
+
install_command = ""
|
|
346
|
+
if dep_file and dep_manager:
|
|
347
|
+
dep_file_path = os.path.join(component_path, dep_file)
|
|
348
|
+
if os.path.exists(dep_file_path):
|
|
349
|
+
print_info(f"Checking dependencies for {component_name}...")
|
|
350
|
+
# Add dependency installation check to command
|
|
351
|
+
if dep_manager == "pip":
|
|
352
|
+
install_command = f"pip install -r {dep_file}"
|
|
353
|
+
elif dep_manager == "npm":
|
|
354
|
+
install_command = f"npm install --legacy-peer-deps"
|
|
355
|
+
elif dep_manager == "flutter":
|
|
356
|
+
install_command = f"flutter pub get"
|
|
357
|
+
else:
|
|
358
|
+
print_warning(f"Dependency file not found: {dep_file_path}")
|
|
359
|
+
|
|
360
|
+
# Build start command with environment activation and dependency installation
|
|
361
|
+
if env_type == "venv" and env_path:
|
|
362
|
+
env_full_path = os.path.join(root_dir, env_path)
|
|
363
|
+
activate_cmd = f"{env_full_path}\\Scripts\\activate" if os.name == 'nt' else f"source {env_full_path}/bin/activate"
|
|
364
|
+
if install_command:
|
|
365
|
+
start_command = f"cd {component_path} && {activate_cmd} && {install_command} && {component_config.get('start_command', '')}"
|
|
366
|
+
else:
|
|
367
|
+
start_command = f"cd {component_path} && {activate_cmd} && {component_config.get('start_command', '')}"
|
|
368
|
+
elif env_type == "conda" and env_path:
|
|
369
|
+
activate_cmd = f"conda activate {env_path}"
|
|
370
|
+
if install_command:
|
|
371
|
+
start_command = f"cd {component_path} && {activate_cmd} && {install_command} && {component_config.get('start_command', '')}"
|
|
372
|
+
else:
|
|
373
|
+
start_command = f"cd {component_path} && {activate_cmd} && {component_config.get('start_command', '')}"
|
|
374
|
+
elif env_type == "node":
|
|
375
|
+
if install_command:
|
|
376
|
+
start_command = f"cd {component_path} && {install_command} && {component_config.get('start_command', '')}"
|
|
377
|
+
else:
|
|
378
|
+
start_command = f"cd {component_path} && {component_config.get('start_command', '')}"
|
|
379
|
+
elif env_type == "flutter":
|
|
380
|
+
if install_command:
|
|
381
|
+
start_command = f"cd {component_path} && {install_command} && {component_config.get('start_command', '')}"
|
|
382
|
+
else:
|
|
383
|
+
start_command = f"cd {component_path} && {component_config.get('start_command', '')}"
|
|
384
|
+
else:
|
|
385
|
+
if install_command:
|
|
386
|
+
start_command = f"cd {component_path} && {install_command} && {component_config.get('start_command', '')}"
|
|
387
|
+
else:
|
|
388
|
+
start_command = f"cd {component_path} && {component_config.get('start_command', '')}"
|
|
389
|
+
|
|
390
|
+
prepared_commands.append(start_command)
|
|
391
|
+
|
|
392
|
+
# Launch services
|
|
393
|
+
if tmux_available:
|
|
394
|
+
# Use tmux for better pane management
|
|
395
|
+
print_info("Using tmux for terminal splitting...")
|
|
396
|
+
|
|
397
|
+
# Start a new tmux session
|
|
398
|
+
session_name = f"{project_name}_services"
|
|
399
|
+
os.system(f"tmux kill-session -t {session_name} 2>/dev/null")
|
|
400
|
+
os.system(f"tmux new-session -d -s {session_name} -c {root_dir}")
|
|
401
|
+
|
|
402
|
+
for i, start_command in enumerate(prepared_commands):
|
|
403
|
+
if i == 0:
|
|
404
|
+
# First pane already exists
|
|
405
|
+
os.system(f"tmux send-keys -t {session_name}:0 '{start_command}' C-m")
|
|
406
|
+
elif i == 1:
|
|
407
|
+
# Split vertically for second pane
|
|
408
|
+
os.system(f"tmux split-window -t {session_name}:0 -v -c {root_dir}")
|
|
409
|
+
os.system(f"tmux send-keys -t {session_name}:0.1 '{start_command}' C-m")
|
|
410
|
+
elif i == 2:
|
|
411
|
+
# Split the left pane horizontally for third pane
|
|
412
|
+
os.system(f"tmux split-window -t {session_name}:0.0 -h -c {root_dir}")
|
|
413
|
+
os.system(f"tmux send-keys -t {session_name}:0.2 '{start_command}' C-m")
|
|
414
|
+
elif i == 3:
|
|
415
|
+
# Split the right pane horizontally for fourth pane
|
|
416
|
+
os.system(f"tmux split-window -t {session_name}:0.1 -h -c {root_dir}")
|
|
417
|
+
os.system(f"tmux send-keys -t {session_name}:0.3 '{start_command}' C-m")
|
|
418
|
+
|
|
419
|
+
# Attach to the tmux session
|
|
420
|
+
os.system(f"tmux attach-session -t {session_name}")
|
|
421
|
+
print_success(f"Launched all services in tmux with 2x2 grid layout")
|
|
422
|
+
elif wt_available:
|
|
423
|
+
# Use Windows Terminal with pane splits
|
|
424
|
+
wt_command = f"wt cmd /k \"{prepared_commands[0]}\""
|
|
425
|
+
|
|
426
|
+
for i, command in enumerate(prepared_commands[1:], 1):
|
|
427
|
+
if i == 1:
|
|
428
|
+
wt_command += f" ; split-pane -V cmd /k \"{command}\""
|
|
429
|
+
else:
|
|
430
|
+
wt_command += f" ; split-pane -H cmd /k \"{command}\""
|
|
431
|
+
|
|
432
|
+
os.system(wt_command)
|
|
433
|
+
print_success(f"Launched all services in Windows Terminal with split panes")
|
|
434
|
+
else:
|
|
435
|
+
# Fallback to opening new terminal windows
|
|
436
|
+
print_warning("Windows Terminal not found. Opening new terminal windows instead.")
|
|
437
|
+
print_info("Install Windows Terminal for pane splitting support.")
|
|
438
|
+
|
|
439
|
+
for start_command in prepared_commands:
|
|
440
|
+
os.system(f"start cmd /k \"{start_command}\"")
|
|
441
|
+
|
|
442
|
+
print()
|
|
443
|
+
print_success(f"All services started for {project_name}")
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from django.contrib import admin
|
|
2
|
+
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
|
3
|
+
from .models import User
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@admin.register(User)
|
|
7
|
+
class UserAdmin(BaseUserAdmin):
|
|
8
|
+
"""Custom User admin."""
|
|
9
|
+
list_display = ('email', 'username', 'first_name', 'last_name', 'is_verified', 'is_staff', 'date_joined')
|
|
10
|
+
list_filter = ('is_staff', 'is_superuser', 'is_active', 'is_verified', 'date_joined')
|
|
11
|
+
search_fields = ('email', 'username', 'first_name', 'last_name')
|
|
12
|
+
ordering = ('-date_joined',)
|
|
13
|
+
|
|
14
|
+
fieldsets = BaseUserAdmin.fieldsets + (
|
|
15
|
+
('Additional Info', {
|
|
16
|
+
'fields': ('phone_number', 'date_of_birth', 'profile_picture', 'is_verified')
|
|
17
|
+
}),
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
add_fieldsets = BaseUserAdmin.add_fieldsets + (
|
|
21
|
+
('Additional Info', {
|
|
22
|
+
'fields': ('email', 'first_name', 'last_name')
|
|
23
|
+
}),
|
|
24
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from django.contrib.auth.models import AbstractUser
|
|
2
|
+
from django.db import models
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class User(AbstractUser):
|
|
6
|
+
"""Custom User model extending AbstractUser."""
|
|
7
|
+
email = models.EmailField(unique=True)
|
|
8
|
+
first_name = models.CharField(max_length=30, blank=True)
|
|
9
|
+
last_name = models.CharField(max_length=30, blank=True)
|
|
10
|
+
phone_number = models.CharField(max_length=15, blank=True)
|
|
11
|
+
date_of_birth = models.DateField(null=True, blank=True)
|
|
12
|
+
profile_picture = models.ImageField(upload_to='profile_pics/', null=True, blank=True)
|
|
13
|
+
is_verified = models.BooleanField(default=False)
|
|
14
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
15
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
16
|
+
|
|
17
|
+
USERNAME_FIELD = 'email'
|
|
18
|
+
REQUIRED_FIELDS = ['username']
|
|
19
|
+
|
|
20
|
+
class Meta:
|
|
21
|
+
db_table = 'auth_user'
|
|
22
|
+
verbose_name = 'User'
|
|
23
|
+
verbose_name_plural = 'Users'
|
|
24
|
+
|
|
25
|
+
def __str__(self):
|
|
26
|
+
return self.email
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def full_name(self):
|
|
30
|
+
return f"{self.first_name} {self.last_name}".strip()
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from rest_framework import serializers
|
|
2
|
+
from django.contrib.auth import authenticate
|
|
3
|
+
from django.contrib.auth.password_validation import validate_password
|
|
4
|
+
from .models import User
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class UserRegistrationSerializer(serializers.ModelSerializer):
|
|
8
|
+
"""Serializer for user registration."""
|
|
9
|
+
password = serializers.CharField(write_only=True, validators=[validate_password])
|
|
10
|
+
password_confirm = serializers.CharField(write_only=True)
|
|
11
|
+
|
|
12
|
+
class Meta:
|
|
13
|
+
model = User
|
|
14
|
+
fields = ('email', 'username', 'password', 'password_confirm', 'first_name', 'last_name')
|
|
15
|
+
|
|
16
|
+
def validate(self, attrs):
|
|
17
|
+
if attrs['password'] != attrs['password_confirm']:
|
|
18
|
+
raise serializers.ValidationError("Passwords don't match.")
|
|
19
|
+
return attrs
|
|
20
|
+
|
|
21
|
+
def create(self, validated_data):
|
|
22
|
+
validated_data.pop('password_confirm')
|
|
23
|
+
user = User.objects.create_user(**validated_data)
|
|
24
|
+
return user
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class UserLoginSerializer(serializers.Serializer):
|
|
28
|
+
"""Serializer for user login."""
|
|
29
|
+
email = serializers.EmailField()
|
|
30
|
+
password = serializers.CharField(write_only=True)
|
|
31
|
+
|
|
32
|
+
def validate(self, attrs):
|
|
33
|
+
email = attrs.get('email')
|
|
34
|
+
password = attrs.get('password')
|
|
35
|
+
|
|
36
|
+
if email and password:
|
|
37
|
+
user = authenticate(username=email, password=password)
|
|
38
|
+
if not user:
|
|
39
|
+
raise serializers.ValidationError('Invalid credentials.')
|
|
40
|
+
if not user.is_active:
|
|
41
|
+
raise serializers.ValidationError('User account is disabled.')
|
|
42
|
+
attrs['user'] = user
|
|
43
|
+
else:
|
|
44
|
+
raise serializers.ValidationError('Must include email and password.')
|
|
45
|
+
|
|
46
|
+
return attrs
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class UserProfileSerializer(serializers.ModelSerializer):
|
|
50
|
+
"""Serializer for user profile."""
|
|
51
|
+
full_name = serializers.ReadOnlyField()
|
|
52
|
+
|
|
53
|
+
class Meta:
|
|
54
|
+
model = User
|
|
55
|
+
fields = ('id', 'email', 'username', 'first_name', 'last_name', 'full_name',
|
|
56
|
+
'phone_number', 'date_of_birth', 'profile_picture', 'is_verified',
|
|
57
|
+
'date_joined', 'created_at', 'updated_at')
|
|
58
|
+
read_only_fields = ('id', 'email', 'date_joined', 'created_at', 'updated_at', 'is_verified')
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class UpdateProfileSerializer(serializers.ModelSerializer):
|
|
62
|
+
"""Serializer for updating user profile."""
|
|
63
|
+
|
|
64
|
+
class Meta:
|
|
65
|
+
model = User
|
|
66
|
+
fields = ('first_name', 'last_name', 'phone_number', 'date_of_birth', 'profile_picture')
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ChangePasswordSerializer(serializers.Serializer):
|
|
70
|
+
"""Serializer for changing password."""
|
|
71
|
+
old_password = serializers.CharField(write_only=True)
|
|
72
|
+
new_password = serializers.CharField(write_only=True, validators=[validate_password])
|
|
73
|
+
new_password_confirm = serializers.CharField(write_only=True)
|
|
74
|
+
|
|
75
|
+
def validate(self, attrs):
|
|
76
|
+
if attrs['new_password'] != attrs['new_password_confirm']:
|
|
77
|
+
raise serializers.ValidationError("New passwords don't match.")
|
|
78
|
+
return attrs
|
|
79
|
+
|
|
80
|
+
def validate_old_password(self, value):
|
|
81
|
+
user = self.context['request'].user
|
|
82
|
+
if not user.check_password(value):
|
|
83
|
+
raise serializers.ValidationError("Old password is incorrect.")
|
|
84
|
+
return value
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class ResetPasswordSerializer(serializers.Serializer):
|
|
88
|
+
"""Serializer for password reset request."""
|
|
89
|
+
email = serializers.EmailField()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class ResetPasswordConfirmSerializer(serializers.Serializer):
|
|
93
|
+
"""Serializer for password reset confirmation."""
|
|
94
|
+
token = serializers.CharField()
|
|
95
|
+
new_password = serializers.CharField(write_only=True, validators=[validate_password])
|
|
96
|
+
new_password_confirm = serializers.CharField(write_only=True)
|
|
97
|
+
|
|
98
|
+
def validate(self, attrs):
|
|
99
|
+
if attrs['new_password'] != attrs['new_password_confirm']:
|
|
100
|
+
raise serializers.ValidationError("Passwords don't match.")
|
|
101
|
+
return attrs
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from django.urls import path
|
|
2
|
+
from . import views
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
app_name = 'authentication'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
urlpatterns = [
|
|
9
|
+
# Add your URL patterns here
|
|
10
|
+
path('login/', views.login, name='login'),
|
|
11
|
+
path('register/', views.register, name='register'),
|
|
12
|
+
path('logout/', views.logout, name='logout'),
|
|
13
|
+
path('refresh/', views.refresh, name='refresh'),
|
|
14
|
+
path('me/', views.me, name='me'),
|
|
15
|
+
path('update-profile/', views.update_profile, name='update_profile'),
|
|
16
|
+
path('change-password/', views.change_password, name='change_password'),
|
|
17
|
+
path('reset-password/', views.reset_password, name='reset_password'),
|
|
18
|
+
path('reset-password-confirm/', views.reset_password_confirm, name='reset_password_confirm'),
|
|
19
|
+
]
|