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
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
from rest_framework import status
|
|
2
|
+
from rest_framework.decorators import api_view, permission_classes
|
|
3
|
+
from rest_framework.permissions import IsAuthenticated, AllowAny
|
|
4
|
+
from rest_framework.response import Response
|
|
5
|
+
from rest_framework_simplejwt.tokens import RefreshToken
|
|
6
|
+
from django.contrib.auth import login as django_login, logout as django_logout
|
|
7
|
+
from django.contrib.auth.tokens import default_token_generator
|
|
8
|
+
from django.core.mail import send_mail
|
|
9
|
+
from django.conf import settings
|
|
10
|
+
from .models import User
|
|
11
|
+
from .serializers import (
|
|
12
|
+
UserRegistrationSerializer, UserLoginSerializer, UserProfileSerializer,
|
|
13
|
+
UpdateProfileSerializer, ChangePasswordSerializer, ResetPasswordSerializer,
|
|
14
|
+
ResetPasswordConfirmSerializer
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_tokens_for_user(user):
|
|
19
|
+
"""Generate JWT tokens for user."""
|
|
20
|
+
refresh = RefreshToken.for_user(user)
|
|
21
|
+
return {
|
|
22
|
+
'refresh': str(refresh),
|
|
23
|
+
'access': str(refresh.access_token),
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@api_view(['POST'])
|
|
28
|
+
@permission_classes([AllowAny])
|
|
29
|
+
def register(request):
|
|
30
|
+
"""User registration endpoint."""
|
|
31
|
+
serializer = UserRegistrationSerializer(data=request.data)
|
|
32
|
+
if serializer.is_valid():
|
|
33
|
+
user = serializer.save()
|
|
34
|
+
tokens = get_tokens_for_user(user)
|
|
35
|
+
return Response({
|
|
36
|
+
'message': 'User registered successfully',
|
|
37
|
+
'user': UserProfileSerializer(user).data,
|
|
38
|
+
'tokens': tokens
|
|
39
|
+
}, status=status.HTTP_201_CREATED)
|
|
40
|
+
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@api_view(['POST'])
|
|
44
|
+
@permission_classes([AllowAny])
|
|
45
|
+
def login(request):
|
|
46
|
+
"""User login endpoint."""
|
|
47
|
+
serializer = UserLoginSerializer(data=request.data)
|
|
48
|
+
if serializer.is_valid():
|
|
49
|
+
user = serializer.validated_data['user']
|
|
50
|
+
django_login(request, user)
|
|
51
|
+
tokens = get_tokens_for_user(user)
|
|
52
|
+
return Response({
|
|
53
|
+
'message': 'Login successful',
|
|
54
|
+
'user': UserProfileSerializer(user).data,
|
|
55
|
+
'tokens': tokens
|
|
56
|
+
}, status=status.HTTP_200_OK)
|
|
57
|
+
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@api_view(['POST'])
|
|
61
|
+
@permission_classes([IsAuthenticated])
|
|
62
|
+
def logout(request):
|
|
63
|
+
"""User logout endpoint."""
|
|
64
|
+
try:
|
|
65
|
+
refresh_token = request.data.get('refresh_token')
|
|
66
|
+
if refresh_token:
|
|
67
|
+
token = RefreshToken(refresh_token)
|
|
68
|
+
token.blacklist()
|
|
69
|
+
django_logout(request)
|
|
70
|
+
return Response({'message': 'Logout successful'}, status=status.HTTP_200_OK)
|
|
71
|
+
except Exception as e:
|
|
72
|
+
return Response({'error': 'Invalid token'}, status=status.HTTP_400_BAD_REQUEST)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@api_view(['POST'])
|
|
76
|
+
@permission_classes([AllowAny])
|
|
77
|
+
def refresh(request):
|
|
78
|
+
"""Token refresh endpoint."""
|
|
79
|
+
try:
|
|
80
|
+
refresh_token = request.data.get('refresh')
|
|
81
|
+
if not refresh_token:
|
|
82
|
+
return Response({'error': 'Refresh token required'}, status=status.HTTP_400_BAD_REQUEST)
|
|
83
|
+
|
|
84
|
+
token = RefreshToken(refresh_token)
|
|
85
|
+
return Response({
|
|
86
|
+
'access': str(token.access_token),
|
|
87
|
+
}, status=status.HTTP_200_OK)
|
|
88
|
+
except Exception as e:
|
|
89
|
+
return Response({'error': 'Invalid refresh token'}, status=status.HTTP_400_BAD_REQUEST)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@api_view(['GET'])
|
|
93
|
+
@permission_classes([IsAuthenticated])
|
|
94
|
+
def me(request):
|
|
95
|
+
"""Get current user profile."""
|
|
96
|
+
serializer = UserProfileSerializer(request.user)
|
|
97
|
+
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@api_view(['PUT', 'PATCH'])
|
|
101
|
+
@permission_classes([IsAuthenticated])
|
|
102
|
+
def update_profile(request):
|
|
103
|
+
"""Update user profile."""
|
|
104
|
+
serializer = UpdateProfileSerializer(request.user, data=request.data, partial=True)
|
|
105
|
+
if serializer.is_valid():
|
|
106
|
+
serializer.save()
|
|
107
|
+
return Response({
|
|
108
|
+
'message': 'Profile updated successfully',
|
|
109
|
+
'user': UserProfileSerializer(request.user).data
|
|
110
|
+
}, status=status.HTTP_200_OK)
|
|
111
|
+
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@api_view(['POST'])
|
|
115
|
+
@permission_classes([IsAuthenticated])
|
|
116
|
+
def change_password(request):
|
|
117
|
+
"""Change user password."""
|
|
118
|
+
serializer = ChangePasswordSerializer(data=request.data, context={'request': request})
|
|
119
|
+
if serializer.is_valid():
|
|
120
|
+
user = request.user
|
|
121
|
+
user.set_password(serializer.validated_data['new_password'])
|
|
122
|
+
user.save()
|
|
123
|
+
return Response({'message': 'Password changed successfully'}, status=status.HTTP_200_OK)
|
|
124
|
+
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@api_view(['POST'])
|
|
128
|
+
@permission_classes([AllowAny])
|
|
129
|
+
def reset_password(request):
|
|
130
|
+
"""Request password reset."""
|
|
131
|
+
serializer = ResetPasswordSerializer(data=request.data)
|
|
132
|
+
if serializer.is_valid():
|
|
133
|
+
email = serializer.validated_data['email']
|
|
134
|
+
try:
|
|
135
|
+
user = User.objects.get(email=email)
|
|
136
|
+
token = default_token_generator.make_token(user)
|
|
137
|
+
|
|
138
|
+
# Send email (you'll need to configure email settings)
|
|
139
|
+
reset_url = f"{settings.FRONTEND_URL}/reset-password-confirm?token={token}&user_id={user.id}"
|
|
140
|
+
send_mail(
|
|
141
|
+
'Password Reset Request',
|
|
142
|
+
f'Click the link to reset your password: {reset_url}',
|
|
143
|
+
settings.DEFAULT_FROM_EMAIL,
|
|
144
|
+
[email],
|
|
145
|
+
fail_silently=False,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return Response({'message': 'Password reset email sent'}, status=status.HTTP_200_OK)
|
|
149
|
+
except User.DoesNotExist:
|
|
150
|
+
# Don't reveal if email exists or not for security
|
|
151
|
+
return Response({'message': 'Password reset email sent'}, status=status.HTTP_200_OK)
|
|
152
|
+
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@api_view(['POST'])
|
|
156
|
+
@permission_classes([AllowAny])
|
|
157
|
+
def reset_password_confirm(request):
|
|
158
|
+
"""Confirm password reset."""
|
|
159
|
+
serializer = ResetPasswordConfirmSerializer(data=request.data)
|
|
160
|
+
if serializer.is_valid():
|
|
161
|
+
token = serializer.validated_data['token']
|
|
162
|
+
new_password = serializer.validated_data['new_password']
|
|
163
|
+
user_id = request.data.get('user_id')
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
user = User.objects.get(id=user_id)
|
|
167
|
+
if default_token_generator.check_token(user, token):
|
|
168
|
+
user.set_password(new_password)
|
|
169
|
+
user.save()
|
|
170
|
+
return Response({'message': 'Password reset successful'}, status=status.HTTP_200_OK)
|
|
171
|
+
else:
|
|
172
|
+
return Response({'error': 'Invalid or expired token'}, status=status.HTTP_400_BAD_REQUEST)
|
|
173
|
+
except User.DoesNotExist:
|
|
174
|
+
return Response({'error': 'Invalid user'}, status=status.HTTP_400_BAD_REQUEST)
|
|
175
|
+
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .main import init_drf_project, init_drf_project_in_current_dir
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
Project/
|
|
2
|
+
├── .env
|
|
3
|
+
├── .gitignore
|
|
4
|
+
├── manage.py
|
|
5
|
+
├── requirements.txt
|
|
6
|
+
├── docker-compose.yml
|
|
7
|
+
├── Dockerfile.
|
|
8
|
+
├── captain-definition.
|
|
9
|
+
├── README.md
|
|
10
|
+
├── {project_name}/
|
|
11
|
+
│ ├── __init__.py
|
|
12
|
+
│ ├── asgi.py
|
|
13
|
+
│ ├── wsgi.py
|
|
14
|
+
│ ├── schema.py
|
|
15
|
+
│ ├── urls.py
|
|
16
|
+
│ ├── celery.py
|
|
17
|
+
│ ├── settings/
|
|
18
|
+
│ │ ├── __init__.py
|
|
19
|
+
│ │ ├── base.py
|
|
20
|
+
│ │ ├── dev.py
|
|
21
|
+
│ │ └── prod.py
|
|
22
|
+
├── apps/
|
|
23
|
+
│ ├── common/
|
|
24
|
+
│ │ ├── models.py
|
|
25
|
+
│ │ ├── serializers/
|
|
26
|
+
│ │ ├── services/
|
|
27
|
+
│ │ ├── tests/
|
|
28
|
+
│ ├── app/
|
|
29
|
+
│ │ ├── __init__.py
|
|
30
|
+
│ │ ├── permissions/
|
|
31
|
+
│ │ ├── serializers/
|
|
32
|
+
│ │ ├── selectors/
|
|
33
|
+
│ │ ├── services/
|
|
34
|
+
│ │ ├── socket/
|
|
35
|
+
│ │ ├── tasks/
|
|
36
|
+
│ │ ├── tests/
|
|
37
|
+
│ │ ├── admin.py
|
|
38
|
+
│ │ ├── apps.py
|
|
39
|
+
│ │ ├── models.py
|
|
40
|
+
│ │ ├── urls.py
|
|
41
|
+
│ │ └── views.py
|
|
42
|
+
│ └── app2/
|
|
43
|
+
│ ├── __init__.py
|
|
44
|
+
│ ├── permissions/
|
|
45
|
+
│ ├── serializers/
|
|
46
|
+
│ ├── selectors/
|
|
47
|
+
│ ├── services/
|
|
48
|
+
│ ├── socket/
|
|
49
|
+
│ ├── tasks/
|
|
50
|
+
│ ├── tests/
|
|
51
|
+
│ ├── admin.py
|
|
52
|
+
│ ├── apps.py
|
|
53
|
+
│ ├── models.py
|
|
54
|
+
│ ├── urls.py
|
|
55
|
+
│ └── views.py
|
|
56
|
+
├── core/
|
|
57
|
+
│ ├── __init__.py
|
|
58
|
+
│ ├── pagination.py
|
|
59
|
+
│ ├── base_model.py
|
|
60
|
+
│ ├── response_wrapper.py
|
|
61
|
+
│ ├── permissions/
|
|
62
|
+
│ ├── exceptions/
|
|
63
|
+
│ ├── signals/
|
|
64
|
+
│ ├── utils/
|
|
65
|
+
│ ├── middlewares/
|
|
66
|
+
│ ├── services/
|
|
67
|
+
│ │ ├── __init__.py
|
|
68
|
+
│ │ ├── azure/
|
|
69
|
+
│ │ │ ├── __init__.py
|
|
70
|
+
│ │ │ ├── email/
|
|
71
|
+
│ │ │ ├── sms/
|
|
72
|
+
│ │ │ ├── storage/
|
|
73
|
+
│ │ │ ├── test.py
|
|
74
|
+
│ │ ├── aws/
|
|
75
|
+
│ │ │ ├── __init__.py
|
|
76
|
+
│ │ │ ├── email/
|
|
77
|
+
│ │ │ ├── sms/
|
|
78
|
+
│ │ │ ├── storage/
|
|
79
|
+
│ │ │ ├── test.py
|
|
80
|
+
│ ├── logging/
|
|
81
|
+
│ │ ├── __init__.py
|
|
82
|
+
│ │ ├── formatter.py
|
|
83
|
+
│ │ ├── logger.py
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
import json
|
|
5
|
+
import secrets
|
|
6
|
+
import shutil
|
|
7
|
+
import platform
|
|
8
|
+
from colorama import Fore, Style
|
|
9
|
+
from ntc.utils.output import print_success, print_error, print_warning, print_info, print_progress, print_package, print_download
|
|
10
|
+
|
|
11
|
+
def sanitize_project_name(name):
|
|
12
|
+
"""Sanitize project name by removing TLD and replacing dots with underscores."""
|
|
13
|
+
# Common TLDs to remove
|
|
14
|
+
tlds = ['.com', '.org', '.net', '.io', '.co', '.app', '.dev', '.tech', '.ai', '.ly', '.me', '.tv', '.co.tz', '.tz', '.or.tz']
|
|
15
|
+
|
|
16
|
+
# Remove TLD if present
|
|
17
|
+
original_name = name
|
|
18
|
+
for tld in tlds:
|
|
19
|
+
if name.lower().endswith(tld):
|
|
20
|
+
name = name[:-len(tld)]
|
|
21
|
+
print_info(f"Removed TLD '{tld}' from project name: '{original_name}' -> '{name}'")
|
|
22
|
+
break
|
|
23
|
+
|
|
24
|
+
# Replace dots with underscores
|
|
25
|
+
if '.' in name:
|
|
26
|
+
name = name.replace('.', '_')
|
|
27
|
+
print_info(f"Replaced dots with underscores in project name: '{name}'")
|
|
28
|
+
|
|
29
|
+
# Remove any other invalid characters and ensure it starts with a letter
|
|
30
|
+
import re
|
|
31
|
+
sanitized = re.sub(r'[^a-zA-Z0-9_]', '_', name)
|
|
32
|
+
if sanitized and sanitized[0].isdigit():
|
|
33
|
+
sanitized = 'project_' + sanitized
|
|
34
|
+
|
|
35
|
+
if sanitized != name:
|
|
36
|
+
print_info(f"Sanitized project name: '{name}' -> '{sanitized}'")
|
|
37
|
+
|
|
38
|
+
return sanitized.lower()
|
|
39
|
+
|
|
40
|
+
def replace_variables(content, variables):
|
|
41
|
+
"""Replace placeholders with actual variables."""
|
|
42
|
+
for key, value in variables.items():
|
|
43
|
+
content = content.replace(f"{{{key}}}", value)
|
|
44
|
+
return content
|
|
45
|
+
|
|
46
|
+
def get_template_content(template_name, variables):
|
|
47
|
+
"""Load template content and replace variables."""
|
|
48
|
+
# Remove 'drf_structure/' prefix if present since we're already in that directory
|
|
49
|
+
if template_name.startswith("drf_structure/"):
|
|
50
|
+
template_name = template_name[len("drf_structure/"):]
|
|
51
|
+
|
|
52
|
+
template_path = os.path.join(os.path.dirname(__file__), template_name)
|
|
53
|
+
if not os.path.exists(template_path):
|
|
54
|
+
return ""
|
|
55
|
+
with open(template_path, "r", encoding="utf-8") as f:
|
|
56
|
+
content = f.read()
|
|
57
|
+
return replace_variables(content, variables)
|
|
58
|
+
|
|
59
|
+
def create_from_json(base_path, structure, variables):
|
|
60
|
+
"""Recursively create directories and files from JSON."""
|
|
61
|
+
for name, content in structure.items():
|
|
62
|
+
name = replace_variables(name, variables)
|
|
63
|
+
full_path = os.path.join(base_path, name)
|
|
64
|
+
|
|
65
|
+
if isinstance(content, dict):
|
|
66
|
+
if "template" in content:
|
|
67
|
+
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
|
68
|
+
with open(full_path, "w", encoding="utf-8") as f:
|
|
69
|
+
f.write(get_template_content(content["template"], variables))
|
|
70
|
+
else:
|
|
71
|
+
os.makedirs(full_path, exist_ok=True)
|
|
72
|
+
create_from_json(full_path, content, variables)
|
|
73
|
+
else:
|
|
74
|
+
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
|
75
|
+
with open(full_path, "w", encoding="utf-8") as f:
|
|
76
|
+
f.write(replace_variables(content or "", variables))
|
|
77
|
+
|
|
78
|
+
def activate_env(env_type, env_dir):
|
|
79
|
+
"""
|
|
80
|
+
Print activation instructions and optionally open a non-blocking new terminal.
|
|
81
|
+
"""
|
|
82
|
+
if env_type == "venv":
|
|
83
|
+
if os.name == "nt":
|
|
84
|
+
activate_cmd = f"{env_dir}\\Scripts\\activate"
|
|
85
|
+
else:
|
|
86
|
+
activate_cmd = f"source {env_dir}/bin/activate"
|
|
87
|
+
else:
|
|
88
|
+
activate_cmd = f"conda activate {env_dir}"
|
|
89
|
+
|
|
90
|
+
print_success("Virtual environment created and dependencies installed!")
|
|
91
|
+
print_info(f"To activate your environment, run: {activate_cmd}")
|
|
92
|
+
|
|
93
|
+
# Optionally open a new terminal (non-blocking) for user convenience
|
|
94
|
+
try:
|
|
95
|
+
if platform.system() == "Windows":
|
|
96
|
+
subprocess.Popen(["start", "cmd", "/k", activate_cmd], shell=True)
|
|
97
|
+
print_info("A new terminal window has been opened with the environment activated.")
|
|
98
|
+
except Exception:
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
def create_virtual_env(env_type, project_root):
|
|
102
|
+
"""
|
|
103
|
+
Creates a virtual environment using venv (default) or conda.
|
|
104
|
+
Skips creation if the environment already exists.
|
|
105
|
+
Returns the path to the environment.
|
|
106
|
+
"""
|
|
107
|
+
env_dir = os.path.join(project_root, "env")
|
|
108
|
+
|
|
109
|
+
if os.path.exists(env_dir):
|
|
110
|
+
print_info(f"Virtual environment already exists at: {env_dir}. Skipping creation.")
|
|
111
|
+
return env_dir
|
|
112
|
+
|
|
113
|
+
if env_type == "venv":
|
|
114
|
+
print_package("Creating virtual environment using venv...")
|
|
115
|
+
try:
|
|
116
|
+
subprocess.run([sys.executable, "-m", "venv", env_dir], check=True)
|
|
117
|
+
print_success(f"Virtual environment created at: {env_dir}")
|
|
118
|
+
except subprocess.CalledProcessError as e:
|
|
119
|
+
print_error(f"Failed to create venv: {e}")
|
|
120
|
+
sys.exit(1)
|
|
121
|
+
elif env_type == "conda":
|
|
122
|
+
conda_executable = find_conda_executable()
|
|
123
|
+
if conda_executable is None:
|
|
124
|
+
print_error("Conda not found. Falling back to venv.")
|
|
125
|
+
subprocess.run([sys.executable, "-m", "venv", env_dir], check=True)
|
|
126
|
+
print_success(f"Virtual environment created at: {env_dir}")
|
|
127
|
+
else:
|
|
128
|
+
print_package("Creating conda environment...")
|
|
129
|
+
try:
|
|
130
|
+
subprocess.run([conda_executable, "create", "-p", env_dir, "python=3.12", "-y"], check=True)
|
|
131
|
+
print_success(f"Conda environment created at: {env_dir}")
|
|
132
|
+
except subprocess.CalledProcessError as e:
|
|
133
|
+
print_error(f"Failed to create conda env: {e}")
|
|
134
|
+
sys.exit(1)
|
|
135
|
+
else:
|
|
136
|
+
print_warning(f"Unknown environment type '{env_type}'. Using venv by default.")
|
|
137
|
+
try:
|
|
138
|
+
subprocess.run([sys.executable, "-m", "venv", env_dir], check=True)
|
|
139
|
+
except subprocess.CalledProcessError as e:
|
|
140
|
+
print_error(f"Failed to create venv: {e}")
|
|
141
|
+
sys.exit(1)
|
|
142
|
+
|
|
143
|
+
return env_dir
|
|
144
|
+
|
|
145
|
+
def update_requirements_versioning(env_type, project_root):
|
|
146
|
+
"""
|
|
147
|
+
Updates requirements.txt after installing dependencies with exact installed versions.
|
|
148
|
+
Works for both venv and conda.
|
|
149
|
+
"""
|
|
150
|
+
# Determine pip path
|
|
151
|
+
if env_type == "venv":
|
|
152
|
+
pip_path = os.path.join(project_root, "env", "Scripts", "pip") if os.name == "nt" else os.path.join(project_root, "env", "bin", "pip")
|
|
153
|
+
elif env_type == "conda":
|
|
154
|
+
pip_path = "pip" # Will run inside conda environment later
|
|
155
|
+
else:
|
|
156
|
+
print(f"⚠️ Unknown env type {env_type}. Skipping requirements update.")
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
# Check if pip exists (only for venv)
|
|
160
|
+
if env_type == "venv" and not os.path.exists(pip_path):
|
|
161
|
+
print("⚠️ pip not found in the environment. Trying to install it...")
|
|
162
|
+
try:
|
|
163
|
+
subprocess.run([sys.executable, "-m", "ensurepip", "--upgrade"], check=True)
|
|
164
|
+
except subprocess.CalledProcessError as e:
|
|
165
|
+
print(f"❌ Could not install pip: {e}")
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
print("📦 Freezing installed packages to requirements.txt...")
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
if env_type == "venv":
|
|
172
|
+
result = subprocess.run(
|
|
173
|
+
[pip_path, "freeze"],
|
|
174
|
+
stdout=subprocess.PIPE,
|
|
175
|
+
stderr=subprocess.PIPE,
|
|
176
|
+
text=True,
|
|
177
|
+
check=True
|
|
178
|
+
)
|
|
179
|
+
else: # conda
|
|
180
|
+
# Run pip freeze inside conda env
|
|
181
|
+
conda_executable = find_conda_executable()
|
|
182
|
+
env_dir = os.path.join(project_root, "env")
|
|
183
|
+
result = subprocess.run(
|
|
184
|
+
[conda_executable, "run", "-p", env_dir, "pip", "freeze"],
|
|
185
|
+
stdout=subprocess.PIPE,
|
|
186
|
+
stderr=subprocess.PIPE,
|
|
187
|
+
text=True,
|
|
188
|
+
check=True
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
requirements_txt = os.path.join(project_root, "requirements.txt")
|
|
192
|
+
with open(requirements_txt, "w", encoding="utf-8") as f:
|
|
193
|
+
f.write(result.stdout)
|
|
194
|
+
|
|
195
|
+
print(f"✅ requirements.txt updated with installed package versions at: {requirements_txt}")
|
|
196
|
+
|
|
197
|
+
except subprocess.CalledProcessError as e:
|
|
198
|
+
print(f"❌ Error freezing packages: {e.stderr}")
|
|
199
|
+
|
|
200
|
+
def find_conda_executable():
|
|
201
|
+
"""Find conda executable on the system."""
|
|
202
|
+
# Common conda locations on Windows
|
|
203
|
+
conda_paths = [
|
|
204
|
+
"conda",
|
|
205
|
+
"conda.exe",
|
|
206
|
+
os.path.expanduser("~/anaconda3/Scripts/conda.exe"),
|
|
207
|
+
os.path.expanduser("~/miniconda3/Scripts/conda.exe"),
|
|
208
|
+
os.path.expanduser("~/AppData/Local/anaconda3/Scripts/conda.exe"),
|
|
209
|
+
os.path.expanduser("~/AppData/Local/miniconda3/Scripts/conda.exe"),
|
|
210
|
+
"C:/ProgramData/Anaconda3/Scripts/conda.exe",
|
|
211
|
+
"C:/ProgramData/Miniconda3/Scripts/conda.exe",
|
|
212
|
+
]
|
|
213
|
+
|
|
214
|
+
for conda_path in conda_paths:
|
|
215
|
+
if shutil.which(conda_path):
|
|
216
|
+
return conda_path
|
|
217
|
+
elif os.path.exists(conda_path):
|
|
218
|
+
return conda_path
|
|
219
|
+
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
def install_requirements(env_type, env_dir, project_root):
|
|
223
|
+
requirements_path = os.path.join(project_root, "requirements.txt")
|
|
224
|
+
|
|
225
|
+
# === Handle conda availability ===
|
|
226
|
+
if env_type == "conda":
|
|
227
|
+
conda_executable = find_conda_executable()
|
|
228
|
+
if conda_executable is None:
|
|
229
|
+
print_error("❌ Conda not found in common locations.")
|
|
230
|
+
print_info("Please ensure conda is installed and accessible.")
|
|
231
|
+
print_info("Falling back to venv...")
|
|
232
|
+
env_type = "venv"
|
|
233
|
+
else:
|
|
234
|
+
print_info(f"Found conda at: {conda_executable}")
|
|
235
|
+
|
|
236
|
+
# === VENV setup ===
|
|
237
|
+
if env_type == "venv":
|
|
238
|
+
if not shutil.which("python"):
|
|
239
|
+
print("❌ Python not found in PATH. Cannot create venv.")
|
|
240
|
+
sys.exit(1)
|
|
241
|
+
|
|
242
|
+
print("📦 Creating virtual environment using venv...")
|
|
243
|
+
subprocess.run([sys.executable, "-m", "venv", env_dir], check=True)
|
|
244
|
+
print(f"✅ Virtual environment created at: {env_dir}")
|
|
245
|
+
|
|
246
|
+
pip_path = os.path.join(env_dir, "Scripts" if os.name == "nt" else "bin", "pip")
|
|
247
|
+
|
|
248
|
+
# Use animated dependency installer for venv
|
|
249
|
+
from ntc.cli_enhanced import animate_dependency_install
|
|
250
|
+
pip_command = [pip_path, "install", "-r", requirements_path]
|
|
251
|
+
animate_dependency_install(requirements_path, pip_command, "venv")
|
|
252
|
+
|
|
253
|
+
update_requirements_versioning(env_type, project_root)
|
|
254
|
+
activate_env(env_type, env_dir)
|
|
255
|
+
return # Stop here after venv setup
|
|
256
|
+
|
|
257
|
+
# === CONDA setup ===
|
|
258
|
+
if env_type == "conda":
|
|
259
|
+
conda_executable = find_conda_executable()
|
|
260
|
+
print("📦 Creating conda environment...")
|
|
261
|
+
subprocess.run([conda_executable, "create", "-p", env_dir, "python=3.12", "-y"], check=True)
|
|
262
|
+
print("✅ Conda environment created successfully.")
|
|
263
|
+
|
|
264
|
+
# Use animated dependency installer for conda
|
|
265
|
+
from ntc.cli_enhanced import animate_dependency_install
|
|
266
|
+
pip_command = [conda_executable, "run", "-p", env_dir, "pip", "install", "-r", requirements_path]
|
|
267
|
+
animate_dependency_install(requirements_path, pip_command, "conda")
|
|
268
|
+
update_requirements_versioning(env_type, project_root)
|
|
269
|
+
activate_env(env_type, env_dir)
|
|
270
|
+
|
|
271
|
+
def get_pip_path(env_dir):
|
|
272
|
+
# Check standard locations for pip inside the venv
|
|
273
|
+
if os.name == "nt":
|
|
274
|
+
pip_path = os.path.join(env_dir, "Scripts", "pip.exe")
|
|
275
|
+
python_path = os.path.join(env_dir, "Scripts", "python.exe")
|
|
276
|
+
else:
|
|
277
|
+
pip_path = os.path.join(env_dir, "bin", "pip")
|
|
278
|
+
python_path = os.path.join(env_dir, "bin", "python")
|
|
279
|
+
|
|
280
|
+
# If pip doesn't exist, bootstrap it using ensurepip
|
|
281
|
+
if not os.path.exists(pip_path):
|
|
282
|
+
print("⚠️ pip not found in the environment. Bootstrapping pip...")
|
|
283
|
+
try:
|
|
284
|
+
subprocess.run([python_path, "-m", "ensurepip", "--upgrade"], check=True)
|
|
285
|
+
except subprocess.CalledProcessError as e:
|
|
286
|
+
print(f"❌ Failed to install pip: {e}")
|
|
287
|
+
return None
|
|
288
|
+
|
|
289
|
+
if os.path.exists(pip_path):
|
|
290
|
+
return pip_path
|
|
291
|
+
else:
|
|
292
|
+
print("❌ pip still not found after ensurepip.")
|
|
293
|
+
return None
|
|
294
|
+
|
|
295
|
+
def init_drf_project(base_path, project_name):
|
|
296
|
+
"""Initialize DRF project structure and setup environment."""
|
|
297
|
+
# Sanitize project name
|
|
298
|
+
original_name = project_name
|
|
299
|
+
project_name = sanitize_project_name(project_name)
|
|
300
|
+
|
|
301
|
+
structure_file = os.path.join(os.path.dirname(__file__), "structure.json")
|
|
302
|
+
|
|
303
|
+
if not os.path.exists(structure_file):
|
|
304
|
+
print_error(f"Structure file not found: {structure_file}")
|
|
305
|
+
return
|
|
306
|
+
|
|
307
|
+
with open(structure_file, "r", encoding="utf-8") as f:
|
|
308
|
+
structure = json.load(f)
|
|
309
|
+
|
|
310
|
+
project_root = os.path.join(base_path, project_name)
|
|
311
|
+
os.makedirs(project_root, exist_ok=True)
|
|
312
|
+
|
|
313
|
+
variables = {
|
|
314
|
+
"project_name": project_name,
|
|
315
|
+
"original_name": original_name,
|
|
316
|
+
"secret_key": secrets.token_urlsafe(50),
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
# Create root-level files (outside the Django project directory)
|
|
320
|
+
root_level_keys = [".env", ".gitignore", "requirements.txt", "CLAUDE.md", "Dockerfile", "docker-compose.yml", ".knowledge", ".entry", "config", "apps", "core"]
|
|
321
|
+
for key in root_level_keys:
|
|
322
|
+
if key in structure:
|
|
323
|
+
create_from_json(project_root, {key: structure[key]}, variables)
|
|
324
|
+
|
|
325
|
+
# Create Django project directory
|
|
326
|
+
django_project_path = os.path.join(project_root, project_name)
|
|
327
|
+
os.makedirs(django_project_path, exist_ok=True)
|
|
328
|
+
create_from_json(django_project_path, structure["{project_name}"], variables)
|
|
329
|
+
|
|
330
|
+
print_success(f"Django DRF project '{project_name}' initialized at: {project_root}")
|
|
331
|
+
|
|
332
|
+
# Use shared environment setup
|
|
333
|
+
from ntc.utils.environment import setup_environment
|
|
334
|
+
setup_environment(project_root)
|
|
335
|
+
|
|
336
|
+
def init_drf_project_in_current_dir(project_name):
|
|
337
|
+
"""Initialize DRF project structure in the current directory."""
|
|
338
|
+
structure_file = os.path.join(os.path.dirname(__file__), "structure.json")
|
|
339
|
+
|
|
340
|
+
if not os.path.exists(structure_file):
|
|
341
|
+
print_error(f"Structure file not found: {structure_file}")
|
|
342
|
+
return
|
|
343
|
+
|
|
344
|
+
with open(structure_file, "r", encoding="utf-8") as f:
|
|
345
|
+
structure = json.load(f)
|
|
346
|
+
|
|
347
|
+
project_root = os.getcwd() # Use current directory as parent
|
|
348
|
+
|
|
349
|
+
project_name = sanitize_project_name(project_name)
|
|
350
|
+
|
|
351
|
+
variables = {
|
|
352
|
+
"project_name": project_name,
|
|
353
|
+
"secret_key": secrets.token_urlsafe(50),
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
# Check if current directory is empty or ask for confirmation
|
|
357
|
+
existing_files = [f for f in os.listdir(project_root) if not f.startswith('.')]
|
|
358
|
+
if existing_files:
|
|
359
|
+
print_warning(f"Current directory is not empty. Found {len(existing_files)} files/folders.")
|
|
360
|
+
print_info("Existing items: " + ", ".join(existing_files[:5]) + ("..." if len(existing_files) > 5 else ""))
|
|
361
|
+
|
|
362
|
+
confirm = input(f"{Fore.WHITE}{Style.BRIGHT}❓ Continue anyway? (y/N): {Style.RESET_ALL}").strip().lower()
|
|
363
|
+
if confirm != 'y':
|
|
364
|
+
print_info("Project initialization cancelled.")
|
|
365
|
+
return
|
|
366
|
+
|
|
367
|
+
# Create root-level files (outside the Django project directory)
|
|
368
|
+
root_level_keys = [".env", ".gitignore", "requirements.txt", "CLAUDE.md", "Dockerfile", "docker-compose.yml", ".knowledge", ".entry", "config", "apps", "core", "manage.py"]
|
|
369
|
+
for key in root_level_keys:
|
|
370
|
+
if key in structure:
|
|
371
|
+
print_info(f"Creating root item: {key}")
|
|
372
|
+
create_from_json(project_root, {key: structure[key]}, variables)
|
|
373
|
+
else:
|
|
374
|
+
print_warning(f"Missing root item in structure: {key}")
|
|
375
|
+
|
|
376
|
+
# Create Django project directory structure
|
|
377
|
+
django_project_path = os.path.join(project_root, project_name)
|
|
378
|
+
os.makedirs(django_project_path, exist_ok=True)
|
|
379
|
+
print_info(f"Creating Django project in: {django_project_path}")
|
|
380
|
+
if "{project_name}" in structure:
|
|
381
|
+
create_from_json(django_project_path, structure["{project_name}"], variables)
|
|
382
|
+
else:
|
|
383
|
+
print_error("Missing {project_name} key in structure!")
|
|
384
|
+
|
|
385
|
+
print_success(f"Django DRF project '{project_name}' initialized in current directory: {project_root}")
|
|
386
|
+
|
|
387
|
+
# Use shared environment setup
|
|
388
|
+
from ntc.utils.environment import setup_environment
|
|
389
|
+
setup_environment(project_root)
|