ntc-dev-kit 1.2.0__tar.gz → 1.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ntc_dev_kit-1.2.0/ntc_dev_kit.egg-info → ntc_dev_kit-1.3.0}/PKG-INFO +81 -2
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/README.md +79 -1
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/__init__.py +1 -1
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/cli_enhanced.py +61 -26
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/auth_app/migrations/0001_initial.py +7 -6
- ntc_dev_kit-1.3.0/ntc/templates/auth_app/models.py +54 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/auth_app/serializers.py +8 -1
- ntc_dev_kit-1.3.0/ntc/templates/auth_app/tasks.py +28 -0
- ntc_dev_kit-1.3.0/ntc/templates/auth_app/templates/authentication/password_reset.html +5 -0
- ntc_dev_kit-1.3.0/ntc/templates/auth_app/templates/authentication/password_reset.txt +9 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/auth_app/tests.py +51 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/auth_app/views.py +25 -11
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/main.py +18 -2
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/structure.json +26 -1
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/.entry/start_tasks_backend.sh +2 -1
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/CLAUDE.md +19 -1
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/apps/common_apps.py +3 -0
- ntc_dev_kit-1.3.0/ntc/templates/drf_structure/templates/apps/common_checks.py +17 -0
- ntc_dev_kit-1.3.0/ntc/templates/drf_structure/templates/ci/github_ci.yml +28 -0
- ntc_dev_kit-1.3.0/ntc/templates/drf_structure/templates/ci/pre_commit.yaml +7 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/core/exceptions/handler.py +5 -8
- ntc_dev_kit-1.3.0/ntc/templates/drf_structure/templates/core/health.py +68 -0
- ntc_dev_kit-1.3.0/ntc/templates/drf_structure/templates/core/renderers.py +15 -0
- ntc_dev_kit-1.3.0/ntc/templates/drf_structure/templates/core/response_wrapper.py +27 -0
- ntc_dev_kit-1.3.0/ntc/templates/drf_structure/templates/core/schema.py +39 -0
- ntc_dev_kit-1.3.0/ntc/templates/drf_structure/templates/core/tests.py +60 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/django_project/django_settings.txt +50 -6
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/django_project/prod_settings.py +5 -0
- ntc_dev_kit-1.3.0/ntc/templates/drf_structure/templates/django_project/schema.py +19 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/django_project/test_settings.py +2 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/django_project/urls.py +4 -1
- ntc_dev_kit-1.3.0/ntc/templates/drf_structure/templates/docker/docker-compose.yml +65 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/knowledge/knowledge_description.md +1 -1
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/knowledge/knowledge_design_pattern.md +12 -19
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/root/README.md +13 -11
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/root/django_requirements.txt +1 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_admin/src/services/api.service.ts +5 -5
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_admin/src/services/base.service.ts +1 -1
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/utils/prompts.py +192 -4
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/wizard.py +82 -38
- ntc_dev_kit-1.3.0/ntc/workspace/__init__.py +2 -0
- ntc_dev_kit-1.3.0/ntc/workspace/clean.py +214 -0
- ntc_dev_kit-1.3.0/ntc/workspace/config.py +342 -0
- ntc_dev_kit-1.3.0/ntc/workspace/create.py +449 -0
- ntc_dev_kit-1.3.0/ntc/workspace/processes.py +307 -0
- ntc_dev_kit-1.3.0/ntc/workspace/screen.py +217 -0
- ntc_dev_kit-1.3.0/ntc/workspace/setup.py +108 -0
- ntc_dev_kit-1.3.0/ntc/workspace/start.py +112 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0/ntc_dev_kit.egg-info}/PKG-INFO +81 -2
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc_dev_kit.egg-info/SOURCES.txt +20 -1
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc_dev_kit.egg-info/requires.txt +3 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/pyproject.toml +2 -1
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/setup.py +2 -1
- ntc_dev_kit-1.3.0/tests/test_workspace.py +96 -0
- ntc_dev_kit-1.2.0/ntc/templates/auth_app/models.py +0 -30
- ntc_dev_kit-1.2.0/ntc/templates/drf_structure/templates/core/response_wrapper.py +0 -12
- ntc_dev_kit-1.2.0/ntc/templates/drf_structure/templates/django_project/schema.py +0 -8
- ntc_dev_kit-1.2.0/ntc/templates/drf_structure/templates/docker/docker-compose.yml +0 -11
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/LICENSE +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/MANIFEST.in +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/app_creator.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/cli.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/composer/__init__.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/composer/main.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/deployment/__init__.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/main.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/__init__.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/auth_app/__init__.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/auth_app/admin.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/auth_app/apps.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/auth_app/urls.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/__init__.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/drf_structure.md +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/.entry/start_core_backend.sh +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/README.md +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/apps/app_admin.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/apps/app_apps.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/apps/app_models.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/apps/app_urls.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/apps/app_views.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/apps/common_models.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/config/gunicorn_config.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/config/http.conf +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/core/formatter.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/core/logger.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/core/pagination.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/core/services/azure/email/backend.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/core/services/azure/email/client.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/core/services/azure/email/notifier.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/core/services/object_storage.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/core/utils/request.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/django_project/asgi.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/django_project/celery.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/django_project/dev_settings.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/django_project/project_init.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/django_project/wsgi.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/docker/Dockerfile +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/docker/captain-definition +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/knowledge/knowledge_structure.md +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/root/django_env.txt +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/root/django_requirements_dev.txt +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/root/dockerignore.txt +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/root/gitattributes.txt +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/root/gitignore.txt +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/root/manage.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/root/pytest_ini.txt +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/drf_structure/templates/root/ruff_toml.txt +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/flutter/.gitignore +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/flutter/.knowledge/knowledge_description.md +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/flutter/.knowledge/knowledge_design_pattern.md +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/flutter/.knowledge/knowledge_structure.md +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/flutter/CLAUDE.md +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/flutter/__init__.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/flutter/analysis_options.yaml +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/flutter/lib/core/config/app_config.dart +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/flutter/lib/core/config/environment.dart +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/flutter/lib/core/constants/app_constants.dart +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/flutter/lib/core/network/dio_client.dart +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/flutter/lib/features/home/presentation/home_screen.dart +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/flutter/lib/l10n/app_localizations.dart +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/flutter/lib/main.dart +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/flutter/lib/services/api_service.dart +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/flutter/lib/shared/localization/locale_provider.dart +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/flutter/lib/shared/theme/app_theme.dart +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/flutter/lib/shared/theme/theme_provider.dart +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/flutter/main.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/flutter/pubspec.yaml +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/flutter/structure.json +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/nextjs/.env.example +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/nextjs/.gitignore +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/nextjs/CLAUDE.md +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/nextjs/README.md +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/nextjs/__init__.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/nextjs/eslint.config.mjs +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/nextjs/knowledge/knowledge_description.md +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/nextjs/knowledge/knowledge_design_pattern.md +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/nextjs/knowledge/knowledge_structure.md +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/nextjs/main.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/nextjs/next.config.js +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/nextjs/package.json +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/nextjs/src/app/globals.css +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/nextjs/src/app/layout.tsx +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/nextjs/src/app/page.tsx +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/nextjs/src/components/Header.tsx +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/nextjs/src/services/api.ts +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/nextjs/src/services/server.ts +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/nextjs/src/utils/constants.ts +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/nextjs/structure.json +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/nextjs/tsconfig.json +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_admin/.env.example +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_admin/.gitignore +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_admin/CLAUDE.md +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_admin/README.md +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_admin/__init__.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_admin/eslint.config.js +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_admin/index.html +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_admin/knowledge/knowledge_description.md +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_admin/knowledge/knowledge_design_pattern.md +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_admin/knowledge/knowledge_structure.md +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_admin/main.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_admin/package.json +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_admin/src/App.tsx +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_admin/src/components/ProtectedRoute.tsx +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_admin/src/components/Sidebar.tsx +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_admin/src/components/TopNav.tsx +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_admin/src/context/AuthContext.tsx +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_admin/src/context/ThemeContext.tsx +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_admin/src/context/WorkspaceContext.tsx +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_admin/src/hooks/useApi.ts +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_admin/src/index.css +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_admin/src/layouts/AuthLayout.tsx +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_admin/src/layouts/DashboardLayout.tsx +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_admin/src/main.tsx +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_admin/src/pages/DashboardPage.tsx +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_admin/src/pages/auth/LoginPage.tsx +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_admin/src/services/token.ts +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_admin/src/vite-env.d.ts +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_admin/structure.json +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_admin/tsconfig.app.json +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_admin/tsconfig.json +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_admin/tsconfig.node.json +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_admin/vite.config.ts +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_structure/App.css +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_structure/CLAUDE.md +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_structure/__init__.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_structure/index.css +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_structure/index.html +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_structure/knowledge/knowledge_description.md +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_structure/knowledge/knowledge_design_pattern.md +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_structure/knowledge/knowledge_structure.md +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_structure/main.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_structure/package.json +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_structure/src/App.tsx +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_structure/src/components/Header.tsx +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_structure/src/main.tsx +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_structure/src/services/api.ts +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_structure/src/utils/constants.ts +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_structure/src/vite-env.d.ts +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_structure/structure.json +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_structure/tsconfig.app.json +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_structure/tsconfig.json +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_structure/tsconfig.node.json +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/templates/react_structure/vite.config.ts +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/ui/__init__.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/ui/app.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/utils/__init__.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/utils/environment.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/utils/output.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/utils/shell.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc/utils/validation.py +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc_dev_kit.egg-info/dependency_links.txt +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc_dev_kit.egg-info/entry_points.txt +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/ntc_dev_kit.egg-info/top_level.txt +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/requirements.txt +0 -0
- {ntc_dev_kit-1.2.0 → ntc_dev_kit-1.3.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ntc-dev-kit
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: Nexent Toolkit Console - A CLI tool for scaffolding Django REST Framework, React, Next.js, and Flutter projects
|
|
5
5
|
Home-page: https://github.com/nexent/ntc
|
|
6
6
|
Author: Nexent
|
|
@@ -27,6 +27,7 @@ License-File: LICENSE
|
|
|
27
27
|
Requires-Dist: colorama>=0.4.6
|
|
28
28
|
Requires-Dist: pyyaml>=6.0
|
|
29
29
|
Requires-Dist: textual>=0.47.0
|
|
30
|
+
Requires-Dist: tomli>=2.0; python_version < "3.11"
|
|
30
31
|
Provides-Extra: dev
|
|
31
32
|
Requires-Dist: build>=1.0; extra == "dev"
|
|
32
33
|
Requires-Dist: twine>=4.0; extra == "dev"
|
|
@@ -121,7 +122,85 @@ ntc init -t nextjs --name site --install --yes
|
|
|
121
122
|
| `--with-auth` / `--no-auth` | Django: include the sign-in app (custom User model, JWT, migration included) |
|
|
122
123
|
| `--env` | Django: `venv`, `conda`, `existing` (the active virtualenv) or `skip` |
|
|
123
124
|
| `--install` / `--no-install` | Others: run `npm install` / `flutter pub get` |
|
|
124
|
-
| `--
|
|
125
|
+
| `--apps` | A workspace instead of one app: `backend,website,dashboard,admin,mobile` (any of them) |
|
|
126
|
+
| `--git` | Workspace: `per-app` (default), `single` or `none` |
|
|
127
|
+
| `--yes`, `-y` | Ask nothing; needs `--name` and `--technology` (or `--apps`) |
|
|
128
|
+
|
|
129
|
+
### Workspaces: several apps at once
|
|
130
|
+
|
|
131
|
+
Tick more than one app at **What are you building?** (Space ticks) - say a backend, a website and
|
|
132
|
+
an admin dashboard - and they're created side by side in one folder, already connected:
|
|
133
|
+
|
|
134
|
+
- each app gets its own port, the frontends point at the backend's API, and the backend allows
|
|
135
|
+
the frontends' origins (CORS);
|
|
136
|
+
- dependencies install for all apps at once, one line each;
|
|
137
|
+
- git: a repository per app (plus a small one at the root for `ntc.toml`), one for everything,
|
|
138
|
+
or none;
|
|
139
|
+
- `ntc.toml` at the root says how each app is set up and run.
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
kaya/
|
|
143
|
+
├── ntc.toml # the apps, their ports, setup and run commands, env
|
|
144
|
+
├── README.md
|
|
145
|
+
├── backend/ # Django REST API http://127.0.0.1:8000
|
|
146
|
+
├── website/ # Next.js http://127.0.0.1:3000
|
|
147
|
+
└── admin/ # React admin http://127.0.0.1:5173
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Then, from anywhere inside the workspace:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
ntc start # every app in this terminal: a tab each plus "All"
|
|
154
|
+
ntc start backend admin # just these (and what they need)
|
|
155
|
+
ntc start --plain # interleaved output instead (CI, logs)
|
|
156
|
+
ntc setup # environments, .env files, dependencies - after a clone or a deep clean
|
|
157
|
+
ntc clean # build output and caches; shows the space freed
|
|
158
|
+
ntc clean --deep # also node_modules and Python environments
|
|
159
|
+
ntc add # another app, wired in like the rest
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
In `ntc start`: **1-9** (or the tabs) switch app, **0** shows all, **r** restarts, **s** stops or
|
|
163
|
+
starts, **o** opens it in the browser, **i** types into it (Flutter's `r` hot reload), **q** quits
|
|
164
|
+
and stops everything. Apps wait for the ones they `need` (the backend) before starting, and a port
|
|
165
|
+
that's already taken is reported instead of crashing.
|
|
166
|
+
|
|
167
|
+
`ntc clean` only removes what the tools rebuild (`.next`, `dist`, `__pycache__`, caches; with
|
|
168
|
+
`--deep` also `node_modules` and `env`), never anything git tracks, and skips apps that are running.
|
|
169
|
+
`--dry-run` shows what would go.
|
|
170
|
+
|
|
171
|
+
`ntc.toml` is plain TOML and meant to be edited and committed (no secrets in it - those stay in
|
|
172
|
+
each app's `.env`):
|
|
173
|
+
|
|
174
|
+
```toml
|
|
175
|
+
name = "kaya"
|
|
176
|
+
|
|
177
|
+
[apps.backend]
|
|
178
|
+
kind = "django"
|
|
179
|
+
path = "backend"
|
|
180
|
+
port = 8000
|
|
181
|
+
python = "env"
|
|
182
|
+
setup = ["{python} -m pip install -r requirements-dev.txt", "{python} manage.py migrate"]
|
|
183
|
+
run = "{python} manage.py runserver 127.0.0.1:{port}"
|
|
184
|
+
# also = { worker = "{python} -m celery -A kaya worker -B -l INFO" }
|
|
185
|
+
|
|
186
|
+
[apps.backend.env]
|
|
187
|
+
CORS_ALLOWED_ORIGINS = "{frontends.origins}"
|
|
188
|
+
|
|
189
|
+
[apps.website]
|
|
190
|
+
kind = "nextjs"
|
|
191
|
+
path = "website"
|
|
192
|
+
port = 3000
|
|
193
|
+
needs = ["backend"]
|
|
194
|
+
run = "npm run dev -- --port {port}"
|
|
195
|
+
repo = "git@github.com:you/website.git" # `ntc setup` clones it when the folder is missing
|
|
196
|
+
|
|
197
|
+
[apps.website.env]
|
|
198
|
+
NEXT_PUBLIC_API_BASE_URL = "{backend.url}/api/v1"
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Placeholders: `{port}`, `{url}`, `{python}` (this app's), `{<app>.port}`, `{<app>.url}`,
|
|
202
|
+
`{frontends.origins}`. Commands are one program with its arguments (no `&&`), so they run the
|
|
203
|
+
same on Windows and Unix.
|
|
125
204
|
|
|
126
205
|
### Technology Options
|
|
127
206
|
|
|
@@ -81,7 +81,85 @@ ntc init -t nextjs --name site --install --yes
|
|
|
81
81
|
| `--with-auth` / `--no-auth` | Django: include the sign-in app (custom User model, JWT, migration included) |
|
|
82
82
|
| `--env` | Django: `venv`, `conda`, `existing` (the active virtualenv) or `skip` |
|
|
83
83
|
| `--install` / `--no-install` | Others: run `npm install` / `flutter pub get` |
|
|
84
|
-
| `--
|
|
84
|
+
| `--apps` | A workspace instead of one app: `backend,website,dashboard,admin,mobile` (any of them) |
|
|
85
|
+
| `--git` | Workspace: `per-app` (default), `single` or `none` |
|
|
86
|
+
| `--yes`, `-y` | Ask nothing; needs `--name` and `--technology` (or `--apps`) |
|
|
87
|
+
|
|
88
|
+
### Workspaces: several apps at once
|
|
89
|
+
|
|
90
|
+
Tick more than one app at **What are you building?** (Space ticks) - say a backend, a website and
|
|
91
|
+
an admin dashboard - and they're created side by side in one folder, already connected:
|
|
92
|
+
|
|
93
|
+
- each app gets its own port, the frontends point at the backend's API, and the backend allows
|
|
94
|
+
the frontends' origins (CORS);
|
|
95
|
+
- dependencies install for all apps at once, one line each;
|
|
96
|
+
- git: a repository per app (plus a small one at the root for `ntc.toml`), one for everything,
|
|
97
|
+
or none;
|
|
98
|
+
- `ntc.toml` at the root says how each app is set up and run.
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
kaya/
|
|
102
|
+
├── ntc.toml # the apps, their ports, setup and run commands, env
|
|
103
|
+
├── README.md
|
|
104
|
+
├── backend/ # Django REST API http://127.0.0.1:8000
|
|
105
|
+
├── website/ # Next.js http://127.0.0.1:3000
|
|
106
|
+
└── admin/ # React admin http://127.0.0.1:5173
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Then, from anywhere inside the workspace:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
ntc start # every app in this terminal: a tab each plus "All"
|
|
113
|
+
ntc start backend admin # just these (and what they need)
|
|
114
|
+
ntc start --plain # interleaved output instead (CI, logs)
|
|
115
|
+
ntc setup # environments, .env files, dependencies - after a clone or a deep clean
|
|
116
|
+
ntc clean # build output and caches; shows the space freed
|
|
117
|
+
ntc clean --deep # also node_modules and Python environments
|
|
118
|
+
ntc add # another app, wired in like the rest
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
In `ntc start`: **1-9** (or the tabs) switch app, **0** shows all, **r** restarts, **s** stops or
|
|
122
|
+
starts, **o** opens it in the browser, **i** types into it (Flutter's `r` hot reload), **q** quits
|
|
123
|
+
and stops everything. Apps wait for the ones they `need` (the backend) before starting, and a port
|
|
124
|
+
that's already taken is reported instead of crashing.
|
|
125
|
+
|
|
126
|
+
`ntc clean` only removes what the tools rebuild (`.next`, `dist`, `__pycache__`, caches; with
|
|
127
|
+
`--deep` also `node_modules` and `env`), never anything git tracks, and skips apps that are running.
|
|
128
|
+
`--dry-run` shows what would go.
|
|
129
|
+
|
|
130
|
+
`ntc.toml` is plain TOML and meant to be edited and committed (no secrets in it - those stay in
|
|
131
|
+
each app's `.env`):
|
|
132
|
+
|
|
133
|
+
```toml
|
|
134
|
+
name = "kaya"
|
|
135
|
+
|
|
136
|
+
[apps.backend]
|
|
137
|
+
kind = "django"
|
|
138
|
+
path = "backend"
|
|
139
|
+
port = 8000
|
|
140
|
+
python = "env"
|
|
141
|
+
setup = ["{python} -m pip install -r requirements-dev.txt", "{python} manage.py migrate"]
|
|
142
|
+
run = "{python} manage.py runserver 127.0.0.1:{port}"
|
|
143
|
+
# also = { worker = "{python} -m celery -A kaya worker -B -l INFO" }
|
|
144
|
+
|
|
145
|
+
[apps.backend.env]
|
|
146
|
+
CORS_ALLOWED_ORIGINS = "{frontends.origins}"
|
|
147
|
+
|
|
148
|
+
[apps.website]
|
|
149
|
+
kind = "nextjs"
|
|
150
|
+
path = "website"
|
|
151
|
+
port = 3000
|
|
152
|
+
needs = ["backend"]
|
|
153
|
+
run = "npm run dev -- --port {port}"
|
|
154
|
+
repo = "git@github.com:you/website.git" # `ntc setup` clones it when the folder is missing
|
|
155
|
+
|
|
156
|
+
[apps.website.env]
|
|
157
|
+
NEXT_PUBLIC_API_BASE_URL = "{backend.url}/api/v1"
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Placeholders: `{port}`, `{url}`, `{python}` (this app's), `{<app>.port}`, `{<app>.url}`,
|
|
161
|
+
`{frontends.origins}`. Commands are one program with its arguments (no `&&`), so they run the
|
|
162
|
+
same on Windows and Unix.
|
|
85
163
|
|
|
86
164
|
### Technology Options
|
|
87
165
|
|
|
@@ -284,8 +284,10 @@ Examples:
|
|
|
284
284
|
ntc init -t django --name shop-api --with-auth --env venv --yes
|
|
285
285
|
# No questions at all (scripts, CI, AI agents)
|
|
286
286
|
ntc init -t nextjs --name site --install --yes
|
|
287
|
-
ntc
|
|
288
|
-
|
|
287
|
+
ntc init --name shop --apps backend,website,admin --yes
|
|
288
|
+
# A workspace: several apps wired together, plus ntc.toml
|
|
289
|
+
ntc start # Run the workspace's apps in this terminal
|
|
290
|
+
ntc setup / ntc clean / ntc add
|
|
289
291
|
ntc create app myapp # Create new Django app
|
|
290
292
|
ntc drf init path name # Legacy Django REST Framework initialization
|
|
291
293
|
"""
|
|
@@ -321,7 +323,11 @@ Examples:
|
|
|
321
323
|
help="Next.js/React/Flutter: don't install dependencies")
|
|
322
324
|
init_parser.add_argument("--yes", "-y", action="store_true",
|
|
323
325
|
help="Don't ask: use the options given and defaults for the rest "
|
|
324
|
-
"(requires --name and --technology)")
|
|
326
|
+
"(requires --name and --technology or --apps)")
|
|
327
|
+
init_parser.add_argument("--apps", help="A workspace of several apps, comma-separated: "
|
|
328
|
+
"backend, website, dashboard, admin, mobile")
|
|
329
|
+
init_parser.add_argument("--git", choices=["per-app", "single", "none"],
|
|
330
|
+
help="Workspace: a repository per app (default), one for everything, or none")
|
|
325
331
|
|
|
326
332
|
# Create command for apps
|
|
327
333
|
create_parser = subparsers.add_parser("create", help="Create new components")
|
|
@@ -345,13 +351,32 @@ Examples:
|
|
|
345
351
|
drf_init_parser.add_argument("name", help="Project name")
|
|
346
352
|
|
|
347
353
|
# Composer command for full-stack project composition
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
354
|
+
subparsers.add_parser("compose", help="Replaced by `ntc init` (pick several apps)")
|
|
355
|
+
add_parser = subparsers.add_parser("add", help="Add apps to the workspace")
|
|
356
|
+
add_parser.add_argument("apps", nargs="*", help="backend, website, dashboard, admin, mobile")
|
|
357
|
+
add_parser.add_argument("--name", help="Folder name (when adding one app)")
|
|
358
|
+
add_parser.add_argument("--env", choices=["venv", "conda", "existing", "skip"], help="Backend: Python environment")
|
|
359
|
+
add_auth = add_parser.add_mutually_exclusive_group()
|
|
360
|
+
add_auth.add_argument("--with-auth", dest="with_auth", action="store_true", default=None)
|
|
361
|
+
add_auth.add_argument("--no-auth", dest="with_auth", action="store_false")
|
|
362
|
+
add_install = add_parser.add_mutually_exclusive_group()
|
|
363
|
+
add_install.add_argument("--install", dest="install", action="store_true", default=None)
|
|
364
|
+
add_install.add_argument("--no-install", dest="install", action="store_false")
|
|
365
|
+
add_parser.add_argument("--yes", "-y", action="store_true", help="Don't ask")
|
|
366
|
+
setup_parser = subparsers.add_parser("setup", help="Install the workspace's apps (after a clone or a deep clean)")
|
|
367
|
+
setup_parser.add_argument("apps", nargs="*", help="Only these apps")
|
|
368
|
+
clean_parser = subparsers.add_parser("clean", help="Delete build output and caches; say how much space it freed")
|
|
369
|
+
clean_parser.add_argument("apps", nargs="*", help="Only these apps")
|
|
370
|
+
clean_parser.add_argument("--deep", action="store_true",
|
|
371
|
+
help="Also dependencies: node_modules, Python environments (`ntc setup` reinstalls)")
|
|
372
|
+
clean_parser.add_argument("--dry-run", action="store_true", help="Show what would go, delete nothing")
|
|
373
|
+
clean_parser.add_argument("--yes", "-y", action="store_true", help="Don't ask before deleting")
|
|
351
374
|
|
|
352
375
|
# Start services command
|
|
353
|
-
start_parser = subparsers.add_parser("start", help="
|
|
354
|
-
start_parser.add_argument("
|
|
376
|
+
start_parser = subparsers.add_parser("start", help="Run the workspace's apps together in this terminal")
|
|
377
|
+
start_parser.add_argument("apps", nargs="*", help="Only these apps (and the ones they need)")
|
|
378
|
+
start_parser.add_argument("--plain", action="store_true",
|
|
379
|
+
help="Interleaved output instead of the full-screen view (CI, logs)")
|
|
355
380
|
|
|
356
381
|
# Doctor command for setup diagnostics
|
|
357
382
|
doctor_parser = subparsers.add_parser("doctor", help="Check setup and fix common issues")
|
|
@@ -364,12 +389,20 @@ Examples:
|
|
|
364
389
|
if args.command == "init":
|
|
365
390
|
technology = args.technology or args.frontend or args.backend
|
|
366
391
|
technology = {"drf": "django", "laravel": "django"}.get(technology, technology)
|
|
367
|
-
|
|
368
|
-
|
|
392
|
+
apps = [a.strip() for a in args.apps.split(",") if a.strip()] if args.apps else None
|
|
393
|
+
if apps and technology:
|
|
394
|
+
print_error("Give either --technology (one app) or --apps (a workspace), not both.")
|
|
369
395
|
sys.exit(2)
|
|
396
|
+
if args.yes and not (args.name and (technology or apps)):
|
|
397
|
+
print_error("--yes needs --name and --technology (or --apps), so there is nothing left to ask.")
|
|
398
|
+
sys.exit(2)
|
|
399
|
+
if apps and len(apps) == 1:
|
|
400
|
+
from ntc.workspace.config import ROLE_KINDS
|
|
401
|
+
technology, apps = ROLE_KINDS.get(apps[0], apps[0]), None
|
|
370
402
|
from ntc.wizard import run as run_wizard
|
|
371
403
|
created = run_wizard(technology=technology, name=args.name, path=args.path, with_auth=args.with_auth,
|
|
372
|
-
env_choice=args.env, install=args.install, assume_yes=args.yes
|
|
404
|
+
env_choice=args.env, install=args.install, assume_yes=args.yes,
|
|
405
|
+
apps=apps, git=args.git)
|
|
373
406
|
sys.exit(0 if created else 1)
|
|
374
407
|
elif args.command == "create" and args.create_type == "app":
|
|
375
408
|
if not is_valid_project_name(args.app_name):
|
|
@@ -385,22 +418,23 @@ Examples:
|
|
|
385
418
|
from ntc.templates.drf_structure.main import init_drf_project
|
|
386
419
|
init_drf_project(args.path, args.name)
|
|
387
420
|
elif args.command == "compose":
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
sys.exit(1)
|
|
392
|
-
from ntc.composer.main import compose_project
|
|
393
|
-
if args.path:
|
|
394
|
-
compose_project(args.project_name, args.path)
|
|
395
|
-
else:
|
|
396
|
-
compose_project(args.project_name)
|
|
421
|
+
print_info("`ntc compose` is now part of `ntc init`: pick several apps (Space ticks them) and they're "
|
|
422
|
+
"created together as a workspace, with an ntc.toml for `ntc start`.")
|
|
423
|
+
sys.exit(2)
|
|
397
424
|
elif args.command == "start":
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
else
|
|
403
|
-
|
|
425
|
+
from ntc.workspace.start import run as run_start
|
|
426
|
+
sys.exit(0 if run_start(args.apps, plain=args.plain) else 1)
|
|
427
|
+
elif args.command == "setup":
|
|
428
|
+
from ntc.workspace.setup import run as run_setup
|
|
429
|
+
sys.exit(0 if run_setup(args.apps) else 1)
|
|
430
|
+
elif args.command == "clean":
|
|
431
|
+
from ntc.workspace.clean import run as run_clean
|
|
432
|
+
sys.exit(0 if run_clean(args.apps, deep=args.deep, assume_yes=args.yes, dry_run=args.dry_run) else 1)
|
|
433
|
+
elif args.command == "add":
|
|
434
|
+
from ntc.workspace.create import run_add
|
|
435
|
+
env_choice = {"existing": "active"}.get(args.env, args.env)
|
|
436
|
+
sys.exit(0 if run_add(args.apps, name=args.name, with_auth=args.with_auth, env_choice=env_choice,
|
|
437
|
+
install=args.install, assume_yes=args.yes) else 1)
|
|
404
438
|
elif args.command == "doctor":
|
|
405
439
|
run_doctor()
|
|
406
440
|
elif args.command == "tui":
|
|
@@ -412,6 +446,7 @@ Examples:
|
|
|
412
446
|
print(f"\n {Fore.LIGHTBLACK_EX}Quick Start:{Style.RESET_ALL}")
|
|
413
447
|
print(f" {Fore.WHITE}ntc tui{Style.RESET_ALL} {Fore.LIGHTBLACK_EX}interactive terminal UI{Style.RESET_ALL}")
|
|
414
448
|
print(f" {Fore.WHITE}ntc init{Style.RESET_ALL} {Fore.LIGHTBLACK_EX}interactive setup{Style.RESET_ALL}")
|
|
449
|
+
print(f" {Fore.WHITE}ntc start{Style.RESET_ALL} {Fore.LIGHTBLACK_EX}run a workspace's apps{Style.RESET_ALL}")
|
|
415
450
|
print(f" {Fore.WHITE}ntc init -t django{Style.RESET_ALL} {Fore.LIGHTBLACK_EX}Django project{Style.RESET_ALL}")
|
|
416
451
|
print(f" {Fore.WHITE}ntc doctor{Style.RESET_ALL} {Fore.LIGHTBLACK_EX}check setup{Style.RESET_ALL}")
|
|
417
452
|
print()
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
# Generated by Django 5.2.7 on 2026-10-20
|
|
1
|
+
# Generated by Django 5.2.7 on 2026-10-20 16:26
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import apps.authentication.models
|
|
4
4
|
import django.contrib.auth.validators
|
|
5
|
+
import django.db.models.functions.text
|
|
5
6
|
import django.utils.timezone
|
|
6
7
|
from django.db import migrations, models
|
|
7
8
|
|
|
@@ -23,12 +24,12 @@ class Migration(migrations.Migration):
|
|
|
23
24
|
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
|
24
25
|
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
|
25
26
|
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
|
27
|
+
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
|
28
|
+
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
|
26
29
|
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
|
27
30
|
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
|
28
31
|
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
|
29
32
|
('email', models.EmailField(max_length=254, unique=True)),
|
|
30
|
-
('first_name', models.CharField(blank=True, max_length=30)),
|
|
31
|
-
('last_name', models.CharField(blank=True, max_length=30)),
|
|
32
33
|
('phone_number', models.CharField(blank=True, max_length=15)),
|
|
33
34
|
('date_of_birth', models.DateField(blank=True, null=True)),
|
|
34
35
|
('profile_picture', models.ImageField(blank=True, null=True, upload_to='profile_pics/')),
|
|
@@ -41,10 +42,10 @@ class Migration(migrations.Migration):
|
|
|
41
42
|
options={
|
|
42
43
|
'verbose_name': 'User',
|
|
43
44
|
'verbose_name_plural': 'Users',
|
|
44
|
-
'
|
|
45
|
+
'constraints': [models.UniqueConstraint(django.db.models.functions.text.Lower('email'), name='authentication_user_email_ci_unique')],
|
|
45
46
|
},
|
|
46
47
|
managers=[
|
|
47
|
-
('objects',
|
|
48
|
+
('objects', apps.authentication.models.UserManager()),
|
|
48
49
|
],
|
|
49
50
|
),
|
|
50
51
|
]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from django.contrib.auth.models import AbstractUser, UserManager as DjangoUserManager
|
|
2
|
+
from django.db import models
|
|
3
|
+
from django.db.models.functions import Lower
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def normalize_email(email):
|
|
7
|
+
"""Emails are stored lowercase: A@x.com and a@x.com are the same person, and sign-in and
|
|
8
|
+
password resets look them up exactly."""
|
|
9
|
+
return (email or '').strip().lower()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class UserManager(DjangoUserManager):
|
|
13
|
+
def _create_user(self, username, email, password, **extra_fields):
|
|
14
|
+
return super()._create_user(username, normalize_email(email), password, **extra_fields)
|
|
15
|
+
|
|
16
|
+
def get_by_natural_key(self, username):
|
|
17
|
+
# USERNAME_FIELD is the email: sign-in matches it whatever the case typed.
|
|
18
|
+
return self.get(email=normalize_email(username))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class User(AbstractUser):
|
|
22
|
+
"""Custom User model extending AbstractUser. first_name/last_name keep AbstractUser's 150 characters."""
|
|
23
|
+
email = models.EmailField(unique=True)
|
|
24
|
+
phone_number = models.CharField(max_length=15, blank=True)
|
|
25
|
+
date_of_birth = models.DateField(null=True, blank=True)
|
|
26
|
+
# Stored in R2 when it's configured (STORAGES in settings), on disk in development.
|
|
27
|
+
profile_picture = models.ImageField(upload_to='profile_pics/', null=True, blank=True)
|
|
28
|
+
is_verified = models.BooleanField(default=False)
|
|
29
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
30
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
31
|
+
|
|
32
|
+
objects = UserManager()
|
|
33
|
+
|
|
34
|
+
USERNAME_FIELD = 'email'
|
|
35
|
+
REQUIRED_FIELDS = ['username']
|
|
36
|
+
|
|
37
|
+
class Meta:
|
|
38
|
+
verbose_name = 'User'
|
|
39
|
+
verbose_name_plural = 'Users'
|
|
40
|
+
constraints = [
|
|
41
|
+
# Belt and braces with the lowercase normalization: no two accounts differ only by case.
|
|
42
|
+
models.UniqueConstraint(Lower('email'), name='authentication_user_email_ci_unique'),
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
def save(self, *args, **kwargs):
|
|
46
|
+
self.email = normalize_email(self.email)
|
|
47
|
+
super().save(*args, **kwargs)
|
|
48
|
+
|
|
49
|
+
def __str__(self):
|
|
50
|
+
return self.email
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def full_name(self) -> str:
|
|
54
|
+
return f"{self.first_name} {self.last_name}".strip()
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from rest_framework import serializers
|
|
2
2
|
from django.contrib.auth import authenticate
|
|
3
3
|
from django.contrib.auth.password_validation import validate_password
|
|
4
|
-
from .models import User
|
|
4
|
+
from .models import User, normalize_email
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class UserRegistrationSerializer(serializers.ModelSerializer):
|
|
@@ -13,6 +13,13 @@ class UserRegistrationSerializer(serializers.ModelSerializer):
|
|
|
13
13
|
model = User
|
|
14
14
|
fields = ('email', 'username', 'password', 'password_confirm', 'first_name', 'last_name')
|
|
15
15
|
|
|
16
|
+
def validate_email(self, value):
|
|
17
|
+
# Case-insensitive, like the database constraint: A@x.com is already taken by a@x.com.
|
|
18
|
+
value = normalize_email(value)
|
|
19
|
+
if User.objects.filter(email=value).exists():
|
|
20
|
+
raise serializers.ValidationError('An account with this email already exists.')
|
|
21
|
+
return value
|
|
22
|
+
|
|
16
23
|
def validate(self, attrs):
|
|
17
24
|
if attrs['password'] != attrs['password_confirm']:
|
|
18
25
|
raise serializers.ValidationError({'password_confirm': "Passwords don't match."})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from celery import shared_task
|
|
4
|
+
|
|
5
|
+
from core.services.azure.email.notifier import send_template_email
|
|
6
|
+
from .models import User
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@shared_task(bind=True, max_retries=3, default_retry_delay=60)
|
|
12
|
+
def send_password_reset_email(self, user_id, reset_url):
|
|
13
|
+
"""The password-reset email, sent by a worker so the request never waits on the mail server.
|
|
14
|
+
Retried a few times if sending fails (the link stays valid for PASSWORD_RESET_TIMEOUT)."""
|
|
15
|
+
user = User.objects.filter(pk=user_id, is_active=True).first()
|
|
16
|
+
if user is None:
|
|
17
|
+
return
|
|
18
|
+
try:
|
|
19
|
+
send_template_email(
|
|
20
|
+
subject='Reset your password',
|
|
21
|
+
to=user.email,
|
|
22
|
+
template='authentication/password_reset',
|
|
23
|
+
context={'user': user, 'reset_url': reset_url},
|
|
24
|
+
sender='no_reply',
|
|
25
|
+
)
|
|
26
|
+
except Exception as exc: # noqa: BLE001 - any mail failure: try again later
|
|
27
|
+
logger.warning('Password reset email to user %s failed: %s', user_id, exc)
|
|
28
|
+
raise self.retry(exc=exc)
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<p>Hi {{ user.first_name|default:user.email }},</p>
|
|
2
|
+
<p>Someone asked to reset the password for your account. If it was you, choose a new password here:</p>
|
|
3
|
+
<p><a href="{{ reset_url }}">Choose a new password</a></p>
|
|
4
|
+
<p>The link works once and expires in a few days. If you didn't ask for this, ignore this email - your
|
|
5
|
+
password stays as it is.</p>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{% autoescape off %}{# plain text: no HTML escaping, or the link shows & #}Hi {{ user.first_name|default:user.email }},
|
|
2
|
+
|
|
3
|
+
Someone asked to reset the password for your account. If it was you, choose a new password here:
|
|
4
|
+
|
|
5
|
+
{{ reset_url }}
|
|
6
|
+
|
|
7
|
+
The link works once and expires in a few days. If you didn't ask for this, ignore this email - your
|
|
8
|
+
password stays as it is.
|
|
9
|
+
{% endautoescape %}
|
|
@@ -104,3 +104,54 @@ def test_sign_in_is_rate_limited_with_a_readable_429(client, user, monkeypatch):
|
|
|
104
104
|
assert response.status_code == 429
|
|
105
105
|
assert response.data['code'] == 'throttled'
|
|
106
106
|
assert 'Retry-After' in response
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_the_client_gets_the_session_inside_the_envelope(client, user):
|
|
110
|
+
body = login(client).json()
|
|
111
|
+
assert body['status'] == 'success'
|
|
112
|
+
assert body['data']['access']
|
|
113
|
+
assert body['data']['user']['email'] == 'ada@example.com'
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_email_is_case_insensitive(client, user):
|
|
117
|
+
response = client.post('/api/v1/auth/register/', {
|
|
118
|
+
'email': ' ADA@Example.com', 'username': 'ada2', 'password': PASSWORD, 'password_confirm': PASSWORD,
|
|
119
|
+
}, format='json')
|
|
120
|
+
assert response.status_code == 400
|
|
121
|
+
assert 'email' in response.json()['errors']
|
|
122
|
+
signed_in = client.post('/api/v1/auth/login/', {'email': 'Ada@EXAMPLE.com', 'password': PASSWORD}, format='json')
|
|
123
|
+
assert signed_in.status_code == 200
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_registration_stores_the_email_lowercase(client, db):
|
|
127
|
+
client.post('/api/v1/auth/register/', {
|
|
128
|
+
'email': 'Grace@Example.com', 'username': 'grace', 'password': PASSWORD, 'password_confirm': PASSWORD,
|
|
129
|
+
}, format='json')
|
|
130
|
+
assert User.objects.filter(email='grace@example.com').exists()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_changing_the_password_signs_out_other_sessions_but_not_this_one(client, user):
|
|
134
|
+
other_device = login(APIClient()).cookies[settings.AUTH_REFRESH_COOKIE].value
|
|
135
|
+
access = login(client).data['access']
|
|
136
|
+
client.credentials(HTTP_AUTHORIZATION=f'Bearer {access}')
|
|
137
|
+
new_password = PASSWORD + '-new'
|
|
138
|
+
response = client.post('/api/v1/auth/change-password/', {
|
|
139
|
+
'old_password': PASSWORD, 'new_password': new_password, 'new_password_confirm': new_password,
|
|
140
|
+
}, format='json')
|
|
141
|
+
assert response.status_code == 200
|
|
142
|
+
assert response.data['access'] # this device stays signed in, with a fresh session
|
|
143
|
+
client.credentials()
|
|
144
|
+
assert client.post('/api/v1/auth/refresh/').status_code == 200
|
|
145
|
+
stolen = APIClient().post('/api/v1/auth/refresh/', {'refresh': other_device}, format='json')
|
|
146
|
+
assert stolen.status_code == 401
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_resetting_the_password_signs_out_every_session(client, user):
|
|
150
|
+
old_session = login(APIClient()).cookies[settings.AUTH_REFRESH_COOKIE].value
|
|
151
|
+
client.post('/api/v1/auth/reset-password/', {'email': 'ada@example.com'}, format='json')
|
|
152
|
+
uid, token = re.search(r'uid=([^&\s]+)&token=([^\s]+)', mail.outbox[0].body).groups()
|
|
153
|
+
new_password = PASSWORD + '-new'
|
|
154
|
+
client.post('/api/v1/auth/reset-password-confirm/', {
|
|
155
|
+
'uid': uid, 'token': token, 'new_password': new_password, 'new_password_confirm': new_password,
|
|
156
|
+
}, format='json')
|
|
157
|
+
assert client.post('/api/v1/auth/refresh/', {'refresh': old_session}, format='json').status_code == 401
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
1
3
|
from django.conf import settings
|
|
2
4
|
from django.contrib.auth.models import update_last_login
|
|
3
5
|
from django.contrib.auth.tokens import default_token_generator
|
|
4
|
-
from django.core.mail import send_mail
|
|
5
6
|
from django.utils.encoding import force_bytes, force_str
|
|
6
7
|
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
|
|
7
8
|
from drf_spectacular.utils import extend_schema, inline_serializer
|
|
@@ -16,11 +17,14 @@ from rest_framework_simplejwt.tokens import RefreshToken
|
|
|
16
17
|
|
|
17
18
|
from core.response_wrapper import error_response
|
|
18
19
|
from .models import User
|
|
20
|
+
from .tasks import send_password_reset_email
|
|
19
21
|
from .serializers import (
|
|
20
22
|
ChangePasswordSerializer, ResetPasswordConfirmSerializer, ResetPasswordSerializer,
|
|
21
23
|
UpdateProfileSerializer, UserLoginSerializer, UserProfileSerializer, UserRegistrationSerializer,
|
|
22
24
|
)
|
|
23
25
|
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
24
28
|
|
|
25
29
|
class AuthRateThrottle(AnonRateThrottle):
|
|
26
30
|
"""Anonymous sign-in, registration and reset attempts per IP (THROTTLE_AUTH): against password guessing."""
|
|
@@ -162,16 +166,26 @@ def update_profile(request):
|
|
|
162
166
|
}, status=status.HTTP_200_OK)
|
|
163
167
|
|
|
164
168
|
|
|
165
|
-
|
|
169
|
+
def revoke_all_sessions(user):
|
|
170
|
+
"""Blacklist every refresh token the user has, so each session ends at its next refresh. Called
|
|
171
|
+
when the password changes: a stolen refresh token must not outlive the password it came with."""
|
|
172
|
+
from rest_framework_simplejwt.token_blacklist.models import BlacklistedToken, OutstandingToken
|
|
173
|
+
tokens = OutstandingToken.objects.filter(user=user).exclude(blacklistedtoken__isnull=False)
|
|
174
|
+
BlacklistedToken.objects.bulk_create([BlacklistedToken(token=t) for t in tokens], ignore_conflicts=True)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@extend_schema(request=ChangePasswordSerializer, responses=SessionSerializer)
|
|
166
178
|
@api_view(['POST'])
|
|
167
179
|
@permission_classes([IsAuthenticated])
|
|
168
180
|
def change_password(request):
|
|
169
|
-
"""Change the password, given the current one.
|
|
181
|
+
"""Change the password, given the current one. Every other session is signed out; this one gets
|
|
182
|
+
a fresh session so the user stays signed in here."""
|
|
170
183
|
serializer = ChangePasswordSerializer(data=request.data, context={'request': request})
|
|
171
184
|
serializer.is_valid(raise_exception=True)
|
|
172
185
|
request.user.set_password(serializer.validated_data['new_password'])
|
|
173
186
|
request.user.save(update_fields=['password'])
|
|
174
|
-
|
|
187
|
+
revoke_all_sessions(request.user)
|
|
188
|
+
return _session(request.user, 'Password changed successfully')
|
|
175
189
|
|
|
176
190
|
|
|
177
191
|
@extend_schema(request=ResetPasswordSerializer, responses=MessageSerializer)
|
|
@@ -188,13 +202,12 @@ def reset_password(request):
|
|
|
188
202
|
uid = urlsafe_base64_encode(force_bytes(user.pk))
|
|
189
203
|
token = default_token_generator.make_token(user)
|
|
190
204
|
reset_url = f"{settings.FRONTEND_URL}/reset-password-confirm?uid={uid}&token={token}"
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
)
|
|
205
|
+
# Sent by a worker (tasks.py): the request doesn't wait on the mail server, and neither a
|
|
206
|
+
# mail outage nor an unreachable queue turns this into a 500 - the answer is the same either way.
|
|
207
|
+
try:
|
|
208
|
+
send_password_reset_email.delay(user.pk, reset_url)
|
|
209
|
+
except Exception: # noqa: BLE001
|
|
210
|
+
logger.exception('Could not queue the password reset email for user %s', user.pk)
|
|
198
211
|
return Response({'message': 'If that email has an account, a reset link is on its way.'},
|
|
199
212
|
status=status.HTTP_200_OK)
|
|
200
213
|
|
|
@@ -215,4 +228,5 @@ def reset_password_confirm(request):
|
|
|
215
228
|
return error_response('This link is invalid or has expired. Please request a new one.', 400)
|
|
216
229
|
user.set_password(serializer.validated_data['new_password'])
|
|
217
230
|
user.save(update_fields=['password'])
|
|
231
|
+
revoke_all_sessions(user) # whoever had the old password is signed out everywhere
|
|
218
232
|
return Response({'message': 'Password reset successful'}, status=status.HTTP_200_OK)
|