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.
Files changed (148) hide show
  1. ntc/__init__.py +5 -0
  2. ntc/app_creator.py +296 -0
  3. ntc/cli.py +4 -0
  4. ntc/cli_enhanced.py +820 -0
  5. ntc/composer/__init__.py +0 -0
  6. ntc/composer/main.py +443 -0
  7. ntc/deployment/__init__.py +0 -0
  8. ntc/templates/__init__.py +2 -0
  9. ntc/templates/auth_app/__init__.py +0 -0
  10. ntc/templates/auth_app/admin.py +24 -0
  11. ntc/templates/auth_app/apps.py +6 -0
  12. ntc/templates/auth_app/models.py +30 -0
  13. ntc/templates/auth_app/serializers.py +101 -0
  14. ntc/templates/auth_app/urls.py +19 -0
  15. ntc/templates/auth_app/views.py +175 -0
  16. ntc/templates/drf_structure/__init__.py +1 -0
  17. ntc/templates/drf_structure/drf_structure.md +83 -0
  18. ntc/templates/drf_structure/main.py +389 -0
  19. ntc/templates/drf_structure/structure.json +126 -0
  20. ntc/templates/drf_structure/templates/.entry/start_core_backend.sh +37 -0
  21. ntc/templates/drf_structure/templates/.entry/start_tasks_backend.sh +30 -0
  22. ntc/templates/drf_structure/templates/CLAUDE.md +194 -0
  23. ntc/templates/drf_structure/templates/README.md +117 -0
  24. ntc/templates/drf_structure/templates/apps/app_admin.py +3 -0
  25. ntc/templates/drf_structure/templates/apps/app_apps.py +6 -0
  26. ntc/templates/drf_structure/templates/apps/app_models.py +4 -0
  27. ntc/templates/drf_structure/templates/apps/app_urls.py +8 -0
  28. ntc/templates/drf_structure/templates/apps/app_views.py +6 -0
  29. ntc/templates/drf_structure/templates/apps/common_apps.py +6 -0
  30. ntc/templates/drf_structure/templates/apps/common_models.py +8 -0
  31. ntc/templates/drf_structure/templates/config/gunicorn_config.py +9 -0
  32. ntc/templates/drf_structure/templates/config/http.conf +33 -0
  33. ntc/templates/drf_structure/templates/core/base_model.py +9 -0
  34. ntc/templates/drf_structure/templates/core/formatter.py +5 -0
  35. ntc/templates/drf_structure/templates/core/logger.py +8 -0
  36. ntc/templates/drf_structure/templates/core/pagination.py +6 -0
  37. ntc/templates/drf_structure/templates/core/response_wrapper.py +7 -0
  38. ntc/templates/drf_structure/templates/core/services/azure/email/backend.py +108 -0
  39. ntc/templates/drf_structure/templates/core/services/azure/email/client.py +296 -0
  40. ntc/templates/drf_structure/templates/core/services/azure/email/notifier.py +102 -0
  41. ntc/templates/drf_structure/templates/core/services/object_storage.py +285 -0
  42. ntc/templates/drf_structure/templates/django_project/asgi.py +5 -0
  43. ntc/templates/drf_structure/templates/django_project/celery.py +8 -0
  44. ntc/templates/drf_structure/templates/django_project/dev_settings.py +2 -0
  45. ntc/templates/drf_structure/templates/django_project/django_settings.txt +322 -0
  46. ntc/templates/drf_structure/templates/django_project/management_command.py +114 -0
  47. ntc/templates/drf_structure/templates/django_project/prod_settings.py +3 -0
  48. ntc/templates/drf_structure/templates/django_project/schema.py +7 -0
  49. ntc/templates/drf_structure/templates/django_project/urls.py +6 -0
  50. ntc/templates/drf_structure/templates/django_project/wsgi.py +5 -0
  51. ntc/templates/drf_structure/templates/docker/docker-compose.yml +11 -0
  52. ntc/templates/drf_structure/templates/knowledge/knowledge_description.md +201 -0
  53. ntc/templates/drf_structure/templates/knowledge/knowledge_design_pattern.md +722 -0
  54. ntc/templates/drf_structure/templates/knowledge/knowledge_structure.md +151 -0
  55. ntc/templates/drf_structure/templates/root/README.md +307 -0
  56. ntc/templates/drf_structure/templates/root/django_env.txt +98 -0
  57. ntc/templates/drf_structure/templates/root/django_requirements.txt +24 -0
  58. ntc/templates/drf_structure/templates/root/gitignore.txt +6 -0
  59. ntc/templates/drf_structure/templates/root/manage.py +11 -0
  60. ntc/templates/flutter/.gitignore +44 -0
  61. ntc/templates/flutter/.knowledge/knowledge_description.md +215 -0
  62. ntc/templates/flutter/.knowledge/knowledge_design_pattern.md +310 -0
  63. ntc/templates/flutter/.knowledge/knowledge_structure.md +83 -0
  64. ntc/templates/flutter/CLAUDE.md +190 -0
  65. ntc/templates/flutter/__init__.py +0 -0
  66. ntc/templates/flutter/analysis_options.yaml +9 -0
  67. ntc/templates/flutter/main.py +187 -0
  68. ntc/templates/flutter/pubspec.yaml +65 -0
  69. ntc/templates/flutter/structure.json +51 -0
  70. ntc/templates/nextjs/.env.example +10 -0
  71. ntc/templates/nextjs/.gitignore +36 -0
  72. ntc/templates/nextjs/CLAUDE.md +181 -0
  73. ntc/templates/nextjs/README.md +94 -0
  74. ntc/templates/nextjs/__init__.py +0 -0
  75. ntc/templates/nextjs/knowledge/knowledge_description.md +208 -0
  76. ntc/templates/nextjs/knowledge/knowledge_design_pattern.md +460 -0
  77. ntc/templates/nextjs/knowledge/knowledge_structure.md +75 -0
  78. ntc/templates/nextjs/main.py +224 -0
  79. ntc/templates/nextjs/package.json +25 -0
  80. ntc/templates/nextjs/src/app/globals.css +26 -0
  81. ntc/templates/nextjs/src/app/layout.tsx +59 -0
  82. ntc/templates/nextjs/src/app/page.tsx +21 -0
  83. ntc/templates/nextjs/src/components/Header.tsx +16 -0
  84. ntc/templates/nextjs/src/services/api.ts +81 -0
  85. ntc/templates/nextjs/src/utils/constants.ts +1 -0
  86. ntc/templates/nextjs/structure.json +32 -0
  87. ntc/templates/nextjs/tsconfig.json +27 -0
  88. ntc/templates/react_admin/.env.example +1 -0
  89. ntc/templates/react_admin/.gitignore +10 -0
  90. ntc/templates/react_admin/CLAUDE.md +210 -0
  91. ntc/templates/react_admin/README.md +72 -0
  92. ntc/templates/react_admin/__init__.py +0 -0
  93. ntc/templates/react_admin/index.html +13 -0
  94. ntc/templates/react_admin/knowledge/knowledge_description.md +202 -0
  95. ntc/templates/react_admin/knowledge/knowledge_design_pattern.md +448 -0
  96. ntc/templates/react_admin/knowledge/knowledge_structure.md +97 -0
  97. ntc/templates/react_admin/main.py +208 -0
  98. ntc/templates/react_admin/package.json +35 -0
  99. ntc/templates/react_admin/src/App.tsx +60 -0
  100. ntc/templates/react_admin/src/components/ProtectedRoute.tsx +16 -0
  101. ntc/templates/react_admin/src/components/Sidebar.tsx +90 -0
  102. ntc/templates/react_admin/src/components/TopNav.tsx +44 -0
  103. ntc/templates/react_admin/src/context/AuthContext.tsx +107 -0
  104. ntc/templates/react_admin/src/context/ThemeContext.tsx +34 -0
  105. ntc/templates/react_admin/src/context/WorkspaceContext.tsx +28 -0
  106. ntc/templates/react_admin/src/hooks/useApi.ts +39 -0
  107. ntc/templates/react_admin/src/index.css +34 -0
  108. ntc/templates/react_admin/src/layouts/AuthLayout.tsx +23 -0
  109. ntc/templates/react_admin/src/layouts/DashboardLayout.tsx +24 -0
  110. ntc/templates/react_admin/src/main.tsx +5 -0
  111. ntc/templates/react_admin/src/pages/DashboardPage.tsx +10 -0
  112. ntc/templates/react_admin/src/pages/auth/LoginPage.tsx +79 -0
  113. ntc/templates/react_admin/src/pages/auth/RegisterPage.tsx +93 -0
  114. ntc/templates/react_admin/src/services/api.service.ts +44 -0
  115. ntc/templates/react_admin/src/services/base.service.ts +193 -0
  116. ntc/templates/react_admin/structure.json +52 -0
  117. ntc/templates/react_admin/tsconfig.app.json +25 -0
  118. ntc/templates/react_admin/tsconfig.json +7 -0
  119. ntc/templates/react_admin/tsconfig.node.json +19 -0
  120. ntc/templates/react_admin/vite.config.ts +13 -0
  121. ntc/templates/react_structure/CLAUDE.md +176 -0
  122. ntc/templates/react_structure/__init__.py +1 -0
  123. ntc/templates/react_structure/index.css +21 -0
  124. ntc/templates/react_structure/index.html +13 -0
  125. ntc/templates/react_structure/knowledge/knowledge_description.md +170 -0
  126. ntc/templates/react_structure/knowledge/knowledge_design_pattern.md +411 -0
  127. ntc/templates/react_structure/knowledge/knowledge_structure.md +74 -0
  128. ntc/templates/react_structure/main.py +217 -0
  129. ntc/templates/react_structure/package.json +32 -0
  130. ntc/templates/react_structure/src/App.tsx +63 -0
  131. ntc/templates/react_structure/src/components/Header.tsx +51 -0
  132. ntc/templates/react_structure/src/main.tsx +5 -0
  133. ntc/templates/react_structure/src/services/api.ts +90 -0
  134. ntc/templates/react_structure/src/utils/constants.ts +1 -0
  135. ntc/templates/react_structure/structure.json +65 -0
  136. ntc/templates/react_structure/tsconfig.app.json +25 -0
  137. ntc/templates/react_structure/tsconfig.json +7 -0
  138. ntc/templates/react_structure/tsconfig.node.json +19 -0
  139. ntc/templates/react_structure/vite.config.ts +12 -0
  140. ntc/utils/__init__.py +1 -0
  141. ntc/utils/environment.py +67 -0
  142. ntc/utils/output.py +41 -0
  143. ntc_dev_kit-1.0.0.dist-info/METADATA +188 -0
  144. ntc_dev_kit-1.0.0.dist-info/RECORD +148 -0
  145. ntc_dev_kit-1.0.0.dist-info/WHEEL +5 -0
  146. ntc_dev_kit-1.0.0.dist-info/entry_points.txt +2 -0
  147. ntc_dev_kit-1.0.0.dist-info/licenses/LICENSE +21 -0
  148. 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)