diff --git a/djinit/cli.py b/djinit/cli.py index 3644db7..53dc429 100644 --- a/djinit/cli.py +++ b/djinit/cli.py @@ -19,12 +19,7 @@ class Cli: self.primary_app = primary_app self.app_names = app_names self.metadata = metadata -<<<<<<< HEAD if project_dir == ".": # handle '.' for current directory -======= - # Handle '.' for current directory - if project_dir == ".": ->>>>>>> origin/main self.project_root = os.getcwd() else: self.project_root = os.path.join(os.getcwd(), project_dir) diff --git a/djinit/scripts/app_generator.py b/djinit/scripts/app_generator.py index 5b46633..a9111aa 100644 --- a/djinit/scripts/app_generator.py +++ b/djinit/scripts/app_generator.py @@ -4,19 +4,14 @@ Handles creation of Django apps and updating settings. """ import os -<<<<<<< HEAD -======= -import subprocess -import sys ->>>>>>> origin/main from typing import Optional from djinit.scripts.console_ui import UIFormatter from djinit.scripts.django_helper import DjangoHelper -from djinit.scripts.template_engine import template_engine from djinit.utils import ( calculate_app_module_path, - create_file_with_content, + create_directory_with_init, + create_file_from_template, create_init_file, detect_nested_structure_from_settings, extract_existing_apps, @@ -32,6 +27,7 @@ class AppManager: self.app_name = app_name self.current_dir = os.getcwd() self.manage_py_path = os.path.join(self.current_dir, "manage.py") + self._project_structure_cache = None def create_app(self) -> bool: if not self._is_django_project(): @@ -61,34 +57,22 @@ class AppManager: if self._is_predefined_structure(): app_path = os.path.join(self.current_dir, "apps", self.app_name) else: - is_nested, nested_dir, apps_base_dir = self._detect_project_structure() + _, _, apps_base_dir = self._get_project_structure() app_path = os.path.join(apps_base_dir, self.app_name) return os.path.exists(app_path) def _create_django_app(self) -> bool: - # Find where to create the app (flat or nested structure) - is_nested, nested_dir, apps_base_dir = self._detect_project_structure() - # Verify manage.py exists in project root - manage_py_path = os.path.join(self.current_dir, "manage.py") - if not os.path.exists(manage_py_path): + if not os.path.exists(self.manage_py_path): UIFormatter.print_error("Could not find manage.py file in project root") return False -<<<<<<< HEAD # If predefined structure detected (apps/ exists), scaffold nested layout using custom templates if self._is_predefined_structure(): return self._create_predefined_app(os.path.join(self.current_dir, "apps")) -======= - # Create app in the correct location - # Use 'python -m django' instead of 'django-admin' for better compatibility - with change_cwd(apps_base_dir): - subprocess.run( - [sys.executable, "-m", "django", "startapp", self.app_name], capture_output=True, text=True, check=True - ) ->>>>>>> origin/main # Otherwise, use standard flat app generation + _, _, apps_base_dir = self._get_project_structure() success = DjangoHelper.startapp(self.app_name, apps_base_dir) if success: UIFormatter.print_success(f"Created Django app '{self.app_name}' in {apps_base_dir}") @@ -115,7 +99,7 @@ class AppManager: if self._is_predefined_structure(): app_module_path = f"apps.{self.app_name}" else: - is_nested, nested_dir, apps_base_dir = self._detect_project_structure() + is_nested, nested_dir, _ = self._get_project_structure() app_module_path = calculate_app_module_path(self.app_name, is_nested, nested_dir) # Check if app is already in USER_DEFINED_APPS @@ -139,15 +123,20 @@ class AppManager: UIFormatter.print_success(f"Added '{app_module_path}' to USER_DEFINED_APPS in base.py") return True - def _detect_project_structure(self) -> tuple[bool, Optional[str], str]: - # Find project directory with settings/base.py - project_dir, settings_base_path = find_project_dir(self.current_dir) + def _get_project_structure(self) -> tuple[bool, Optional[str], str]: + """Get project structure with caching to avoid repeated detection.""" + if self._project_structure_cache is None: + # Find project directory with settings/base.py + project_dir, settings_base_path = find_project_dir(self.current_dir) - if project_dir is None: - return False, None, self.current_dir - - # Detect nested structure from settings file - return detect_nested_structure_from_settings(settings_base_path, self.current_dir) + if project_dir is None: + self._project_structure_cache = (False, None, self.current_dir) + else: + # Detect nested structure from settings file + self._project_structure_cache = detect_nested_structure_from_settings( + settings_base_path, self.current_dir + ) + return self._project_structure_cache def _is_predefined_structure(self) -> bool: # Heuristic: presence of 'apps' directory at project root (where manage.py lives) and 'api' dir @@ -161,15 +150,18 @@ class AppManager: os.makedirs(app_dir, exist_ok=True) create_init_file(app_dir, f"Created apps/{self.app_name}/__init__.py") - # apps.py using standard template - apps_py_content = template_engine.render_template("base/apps.j2", {"app_name": self.app_name}) - create_file_with_content( - os.path.join(app_dir, "apps.py"), apps_py_content, f"Created apps/{self.app_name}/apps.py" - ) - # Compute names for generic templates model_class_name = "".join([part.capitalize() for part in self.app_name.split("_")]) app_module = f"apps.{self.app_name}" + base_context = {"app_name": self.app_name, "app_module": app_module, "model_class_name": model_class_name} + + # Create apps.py + create_file_from_template( + os.path.join(app_dir, "apps.py"), + "base/apps.j2", + {"app_name": self.app_name}, + f"Created apps/{self.app_name}/apps.py", + ) # Subfolders subfolders = { @@ -181,25 +173,19 @@ class AppManager: } for folder, files in subfolders.items(): folder_path = os.path.join(app_dir, folder) - os.makedirs(folder_path, exist_ok=True) - create_init_file(folder_path, f"Created apps/{self.app_name}/{folder}/__init__.py") - for filename, tpl in files: - content = template_engine.render_template( - tpl, - {"app_name": self.app_name, "app_module": app_module, "model_class_name": model_class_name}, - ) - create_file_with_content( - os.path.join(folder_path, filename), - content, - f"Created apps/{self.app_name}/{folder}/{filename}", + create_directory_with_init(folder_path, f"Created apps/{self.app_name}/{folder}/__init__.py") + for filename, template in files: + file_path = os.path.join(folder_path, filename) + create_file_from_template( + file_path, template, base_context, f"Created apps/{self.app_name}/{folder}/{filename}" ) # urls.py at app root - urls_content = template_engine.render_template( - "predefined/apps/generic/urls.j2", {"app_name": self.app_name, "app_module": app_module} - ) - create_file_with_content( - os.path.join(app_dir, "urls.py"), urls_content, f"Created apps/{self.app_name}/urls.py" + create_file_from_template( + os.path.join(app_dir, "urls.py"), + "predefined/apps/generic/urls.j2", + {"app_name": self.app_name, "app_module": app_module}, + f"Created apps/{self.app_name}/urls.py", ) # Add route include to api/v1/urls.py if present diff --git a/djinit/scripts/django_helper.py b/djinit/scripts/django_helper.py index b20fc82..207c3de 100644 --- a/djinit/scripts/django_helper.py +++ b/djinit/scripts/django_helper.py @@ -7,7 +7,12 @@ This module provides functions to create Django projects and apps without requir import os from djinit.scripts.console_ui import UIFormatter -from djinit.utils import create_file_from_template, create_init_file +from djinit.utils import ( + create_directory_with_init, + create_file_from_template, + create_files_from_templates, + create_init_file, +) class DjangoHelper: @@ -32,54 +37,28 @@ class DjangoHelper: # Create project config directory project_config_dir = os.path.join(directory, project_name) - os.makedirs(project_config_dir, exist_ok=True) - create_init_file(project_config_dir, f"Created {project_name}/__init__.py") + create_directory_with_init(project_config_dir, f"Created {project_name}/__init__.py") # Create settings directory and files settings_dir = os.path.join(project_config_dir, "settings") - os.makedirs(settings_dir, exist_ok=True) - create_init_file(settings_dir, f"Created {project_name}/settings/__init__.py") + create_directory_with_init(settings_dir, f"Created {project_name}/settings/__init__.py") base_context = {"project_name": project_name, "app_names": []} - create_file_from_template( - os.path.join(settings_dir, "base.py"), - "project/settings/base.j2", - base_context, - f"Created {project_name}/settings/base.py", - ) - create_file_from_template( - os.path.join(settings_dir, "development.py"), - "project/settings/development.j2", - {}, - f"Created {project_name}/settings/development.py", - ) - create_file_from_template( - os.path.join(settings_dir, "production.py"), - "project/settings/production.j2", - {}, - f"Created {project_name}/settings/production.py", - ) + settings_files = [ + ("base.py", "project/settings/base.j2", base_context), + ("development.py", "project/settings/development.j2", {}), + ("production.py", "project/settings/production.j2", {}), + ] + create_files_from_templates(settings_dir, settings_files, f"{project_name}/settings/") # Create project-level files urls_context = {"project_name": project_name, "django_version": DjangoHelper.DJANGO_VERSION} - create_file_from_template( - os.path.join(project_config_dir, "urls.py"), - "project/urls.j2", - urls_context, - f"Created {project_name}/urls.py", - ) - create_file_from_template( - os.path.join(project_config_dir, "wsgi.py"), - "project/wsgi.j2", - {}, - f"Created {project_name}/wsgi.py", - ) - create_file_from_template( - os.path.join(project_config_dir, "asgi.py"), - "project/asgi.j2", - {}, - f"Created {project_name}/asgi.py", - ) + project_files = [ + ("urls.py", "project/urls.j2", urls_context), + ("wsgi.py", "project/wsgi.j2", {}), + ("asgi.py", "project/asgi.j2", {}), + ] + create_files_from_templates(project_config_dir, project_files, f"{project_name}/") return True @@ -99,28 +78,21 @@ class DjangoHelper: create_init_file(app_dir, f"Created {app_name}/__init__.py") app_files = [ - ("apps.py", "base/apps.j2"), - ("models.py", "base/models.j2"), - ("views.py", "base/views.j2"), - ("admin.py", "base/admin.j2"), - ("urls.py", "base/urls.j2"), - ("serializers.py", "base/serializers.j2"), - ("routes.py", "base/routes.j2"), - ("tests.py", "base/tests.j2"), + ("apps.py", "base/apps.j2", context), + ("models.py", "base/models.j2", context), + ("views.py", "base/views.j2", context), + ("admin.py", "base/admin.j2", context), + ("urls.py", "base/urls.j2", context), + ("serializers.py", "base/serializers.j2", context), + ("routes.py", "base/routes.j2", context), + ("tests.py", "base/tests.j2", context), ] - for filename, template_path in app_files: - create_file_from_template( - os.path.join(app_dir, filename), - template_path, - context, - f"Created {app_name}/{filename}", - ) + create_files_from_templates(app_dir, app_files, f"{app_name}/") # Create migrations directory and __init__.py migrations_dir = os.path.join(app_dir, "migrations") - os.makedirs(migrations_dir, exist_ok=True) - create_init_file(migrations_dir, f"Created {app_name}/migrations/__init__.py") + create_directory_with_init(migrations_dir, f"Created {app_name}/migrations/__init__.py") return True diff --git a/djinit/scripts/files.py b/djinit/scripts/files.py index 313415a..b3d7fdc 100644 --- a/djinit/scripts/files.py +++ b/djinit/scripts/files.py @@ -9,8 +9,11 @@ from djinit.scripts.template_engine import template_engine from djinit.utils import ( calculate_app_module_paths, change_cwd, + create_directory_with_init, + create_file_from_template, create_file_with_content, - create_init_file, + create_files_from_templates, + get_package_name, ) @@ -42,21 +45,29 @@ class FileManager: content = template_engine.render_template(template_path, context) return create_file_with_content(filepath, content, message, should_format=should_format) - def _ensure_directory_with_init(self, dir_path: str, message: str) -> None: - """Helper to create directory and __init__.py file.""" - os.makedirs(dir_path, exist_ok=True) - create_init_file(dir_path, message) - def _create_files_from_specs(self, base_dir: str, folder_specs: dict, base_context: dict = None) -> None: """Helper to create multiple folders and files from a specification dict.""" base_context = base_context or {} for folder, files in folder_specs.items(): folder_path = os.path.join(base_dir, folder) - self._ensure_directory_with_init(folder_path, f"Created {folder_path}/__init__.py") + create_directory_with_init(folder_path, f"Created {folder_path}/__init__.py") for filename, template_path in files: file_path = os.path.join(folder_path, filename) - content = template_engine.render_template(template_path, base_context) - create_file_with_content(file_path, content, f"Created {file_path}") + create_file_from_template(file_path, template_path, base_context, f"Created {file_path}") + + def _create_apps_py(self, app_dir: str, app_name: str, message: str = None, should_format: bool = False) -> None: + """Helper to create apps.py file for an app.""" + apps_py_path = os.path.join(app_dir, "apps.py") + context = {"app_name": app_name} if app_name else {} + msg = message or f"Created {os.path.basename(app_dir)}/apps.py" + create_file_from_template(apps_py_path, "base/apps.j2", context, msg, should_format=should_format) + + def _create_subdirectories_with_init(self, base_dir: str, subdirs: list, prefix: str = "") -> None: + """Helper to create multiple subdirectories with __init__.py files.""" + for subdir in subdirs: + subdir_path = os.path.join(base_dir, subdir) + message = f"Created {prefix}{subdir}/__init__.py" if prefix else f"Created {subdir}/__init__.py" + create_directory_with_init(subdir_path, message) def create_gitignore(self) -> bool: return self._render_and_create_file(".gitignore", "shared/gitignore.j2", {}, "Created .gitignore file") @@ -96,8 +107,7 @@ class FileManager: def create_pyproject_toml(self, metadata: dict) -> bool: # Default package_name to "backend" if it's "." or empty package_name = metadata.get("package_name", "backend") - if package_name == "." or not package_name: - package_name = "backend" + package_name = get_package_name(package_name) context = {"package_name": package_name, "project_name": self.project_name} return self._render_and_create_file( "pyproject.toml", @@ -134,16 +144,12 @@ class FileManager: def create_predefined_structure(self) -> bool: # Ensure apps package apps_dir = os.path.join(self.project_root, "apps") - self._ensure_directory_with_init(apps_dir, "Created apps/__init__.py") + create_directory_with_init(apps_dir, "Created apps/__init__.py") # users app users_dir = os.path.join(apps_dir, "users") - self._ensure_directory_with_init(users_dir, "Created apps/users/__init__.py") - - # apps.py can reuse standard app template - users_apps_path = os.path.join(users_dir, "apps.py") - content = template_engine.render_template("base/apps.j2", {"app_name": "users"}) - create_file_with_content(users_apps_path, content, "Created apps/users/apps.py") + create_directory_with_init(users_dir, "Created apps/users/__init__.py") + self._create_apps_py(users_dir, "users") # Nested folders and files for users users_subfolders = { @@ -157,12 +163,13 @@ class FileManager: # users urls.py at app root users_urls_path = os.path.join(users_dir, "urls.py") - content = template_engine.render_template("predefined/apps/users/urls.j2", {"app_module": "apps.users"}) - create_file_with_content(users_urls_path, content, "Created apps/users/urls.py") + create_file_from_template( + users_urls_path, "predefined/apps/users/urls.j2", {"app_module": "apps.users"}, "Created apps/users/urls.py" + ) # core app core_dir = os.path.join(apps_dir, "core") - self._ensure_directory_with_init(core_dir, "Created apps/core/__init__.py") + create_directory_with_init(core_dir, "Created apps/core/__init__.py") # core subfolders and files core_subfolders = { @@ -174,28 +181,30 @@ class FileManager: # core/exceptions.py exceptions_path = os.path.join(core_dir, "exceptions.py") - content = template_engine.render_template("predefined/core/exceptions.j2", {}) - create_file_with_content(exceptions_path, content, "Created apps/core/exceptions.py") + create_file_from_template( + exceptions_path, "predefined/core/exceptions.j2", {}, "Created apps/core/exceptions.py" + ) # api package api_dir = os.path.join(self.project_root, "api") - self._ensure_directory_with_init(api_dir, "Created api/__init__.py") + create_directory_with_init(api_dir, "Created api/__init__.py") api_urls_path = os.path.join(api_dir, "urls.py") - content = template_engine.render_template("predefined/api/urls.j2", {}) - create_file_with_content(api_urls_path, content, "Created api/urls.py") + create_file_from_template(api_urls_path, "predefined/api/urls.j2", {}, "Created api/urls.py") # api/v1 api_v1_dir = os.path.join(api_dir, "v1") - self._ensure_directory_with_init(api_v1_dir, "Created api/v1/__init__.py") + create_directory_with_init(api_v1_dir, "Created api/v1/__init__.py") api_v1_urls_path = os.path.join(api_v1_dir, "urls.py") - content = template_engine.render_template("predefined/api/v1/urls.j2", {}) - create_file_with_content(api_v1_urls_path, content, "Created api/v1/urls.py") + create_file_from_template(api_v1_urls_path, "predefined/api/v1/urls.j2", {}, "Created api/v1/urls.py") # overwrite project urls to include api project_urls_path = os.path.join(self.project_configs, "urls.py") - content = template_engine.render_template("project/urls_with_api.j2", {"api_module": "api"}) - create_file_with_content( - project_urls_path, content, "Updated project urls.py to include API routes", should_format=True + create_file_from_template( + project_urls_path, + "project/urls_with_api.j2", + {"api_module": "api"}, + "Updated project urls.py to include API routes", + should_format=True, ) return True @@ -203,11 +212,11 @@ class FileManager: def create_unified_structure(self) -> bool: # Create core directory structure core_dir = os.path.join(self.project_root, "core") - self._ensure_directory_with_init(core_dir, "Created core/__init__.py") + create_directory_with_init(core_dir, "Created core/__init__.py") # Create core/settings directory settings_dir = os.path.join(core_dir, "settings") - self._ensure_directory_with_init(settings_dir, "Created core/settings/__init__.py") + create_directory_with_init(settings_dir, "Created core/settings/__init__.py") # Generate secret key for development settings from djinit.scripts.secretkey_generator import generate_secret_key @@ -230,8 +239,10 @@ class FileManager: ("production.py", base_context), ]: filepath = os.path.join(settings_dir, filename) - content = template_engine.render_template(f"project/settings/{filename.replace('.py', '')}.j2", context) - create_file_with_content(filepath, content, f"Created core/settings/{filename}", should_format=True) + template_path = f"project/settings/{filename.replace('.py', '')}.j2" + create_file_from_template( + filepath, template_path, context, f"Created core/settings/{filename}", should_format=True + ) # Core project files core_files = [ @@ -241,66 +252,54 @@ class FileManager: ] for filename, template, context in core_files: filepath = os.path.join(core_dir, filename) - content = template_engine.render_template(template, context) - create_file_with_content(filepath, content, f"Created core/{filename}", should_format=True) + create_file_from_template(filepath, template, context, f"Created core/{filename}", should_format=True) # Create apps directory apps_dir = os.path.join(self.project_root, "apps") - self._ensure_directory_with_init(apps_dir, "Created apps/__init__.py") + create_directory_with_init(apps_dir, "Created apps/__init__.py") # Create apps/core apps_core_dir = os.path.join(apps_dir, "core") - self._ensure_directory_with_init(apps_core_dir, "Created apps/core/__init__.py") + create_directory_with_init(apps_core_dir, "Created apps/core/__init__.py") # Create apps/core/apps.py - apps_py_path = os.path.join(apps_core_dir, "apps.py") - content = template_engine.render_template("base/apps.j2", {}) - create_file_with_content(apps_py_path, content, "Created apps/core/apps.py", should_format=True) + self._create_apps_py(apps_core_dir, "", "Created apps/core/apps.py", should_format=True) # Create apps/core/models models_dir = os.path.join(apps_core_dir, "models") - os.makedirs(models_dir, exist_ok=True) model_files = [ ("__init__.py", "base/models.j2"), ("base.py", "unified/apps/core/models/base.py.j2"), ("mixins.py", "unified/apps/core/models/mixins.py.j2"), ] - for filename, template in model_files: - filepath = os.path.join(models_dir, filename) - content = template_engine.render_template(template, {}) - create_file_with_content(filepath, content, f"Created apps/core/models/{filename}", should_format=True) + os.makedirs(models_dir, exist_ok=True) + create_files_from_templates( + models_dir, [(f, t, {}) for f, t in model_files], "apps/core/models/", should_format=True + ) # Create apps/core/utils utils_dir = os.path.join(apps_core_dir, "utils") - os.makedirs(utils_dir, exist_ok=True) utils_files = [ ("__init__.py", "unified/apps/core/utils/__init__.py.j2"), ("responses.py", "unified/apps/core/utils/responses.py.j2"), ] - for filename, template in utils_files: - filepath = os.path.join(utils_dir, filename) - content = template_engine.render_template(template, {}) - create_file_with_content(filepath, content, f"Created apps/core/utils/{filename}", should_format=True) + os.makedirs(utils_dir, exist_ok=True) + create_files_from_templates( + utils_dir, [(f, t, {}) for f, t in utils_files], "apps/core/utils/", should_format=True + ) # Create apps/core/permissions and middleware - for subdir in ["permissions", "middleware"]: - subdir_path = os.path.join(apps_core_dir, subdir) - self._ensure_directory_with_init(subdir_path, f"Created apps/core/{subdir}/__init__.py") + self._create_subdirectories_with_init(apps_core_dir, ["permissions", "middleware"], "apps/core/") # Create apps/api apps_api_dir = os.path.join(apps_dir, "api") - self._ensure_directory_with_init(apps_api_dir, "Created apps/api/__init__.py") - - # Create apps/api/apps.py - apps_py_path = os.path.join(apps_api_dir, "apps.py") - content = template_engine.render_template("base/apps.j2", {"app_name": "apps.api"}) - create_file_with_content(apps_py_path, content, "Created apps/api/apps.py", should_format=True) + create_directory_with_init(apps_api_dir, "Created apps/api/__init__.py") + self._create_apps_py(apps_api_dir, "apps.api", "Created apps/api/apps.py", should_format=True) # Create apps/api subdirectories - api_subdirs = ["models", "serializers", "views", "services", "tests", "urls", "admin"] - for subdir in api_subdirs: - subdir_path = os.path.join(apps_api_dir, subdir) - self._ensure_directory_with_init(subdir_path, f"Created apps/api/{subdir}/__init__.py") + self._create_subdirectories_with_init( + apps_api_dir, ["models", "serializers", "views", "services", "tests", "urls", "admin"], "apps/api/" + ) return True diff --git a/djinit/scripts/input_handler.py b/djinit/scripts/input_handler.py index 01ecd6b..c6645ed 100644 --- a/djinit/scripts/input_handler.py +++ b/djinit/scripts/input_handler.py @@ -13,6 +13,7 @@ from typing import Callable, Tuple from djinit.scripts.console_ui import UIColors, UIFormatter, console from djinit.scripts.name_validator import validate_app_name, validate_project_name +from djinit.utils import get_package_name class CICDOption(Enum): @@ -69,7 +70,7 @@ class InputCollector: MAX_ATTEMPTS = 3 def __init__(self): - self.char_reader = CharReader() + pass def get_validated_input( self, prompt: str, validator: Callable[[str], Tuple[bool, str]], input_type: str, allow_empty: bool = False @@ -204,10 +205,6 @@ class InputCollector: console.print(f" [{UIColors.SUCCESS}]{option.key}[/{UIColors.SUCCESS}] {option.description}") console.print() -<<<<<<< HEAD -======= - # Get valid keys from CICDOption enum ->>>>>>> origin/main valid_keys = [option.key for option in CICDOption] choice = CharReader.get_cicd_choice( @@ -216,24 +213,12 @@ class InputCollector: default=CICDOption.NONE.key, ) -<<<<<<< HEAD choice_map = { CICDOption.BOTH.key: (True, True), CICDOption.GITHUB.key: (True, False), CICDOption.GITLAB.key: (False, True), } return choice_map.get(choice, (False, False)) -======= - # Parse choice - if choice == CICDOption.BOTH.key: - return True, True - elif choice == CICDOption.GITHUB.key: - return True, False - elif choice == CICDOption.GITLAB.key: - return False, True - else: # Default to none (should not happen due to validation, but kept as fallback) - return False, False ->>>>>>> origin/main def get_nested_apps_config(self) -> Tuple[bool, str | None]: UIFormatter.print_separator() @@ -242,13 +227,7 @@ class InputCollector: ) console.print() -<<<<<<< HEAD choice = CharReader.get_yes_no(f"[{UIColors.HIGHLIGHT}]Nested Django apps? (y/N):[/{UIColors.HIGHLIGHT}]") -======= - choice = CharReader.get_yes_no( - f"[{UIColors.HIGHLIGHT}]Nested Django apps? (y/N):[/{UIColors.HIGHLIGHT}]", default="n" - ) ->>>>>>> origin/main if choice != "y": return False, None @@ -268,16 +247,39 @@ class InputCollector: console.print(f" [{UIColors.SUCCESS}]N[/{UIColors.SUCCESS}] Use individual database parameters") console.print() -<<<<<<< HEAD return CharReader.get_yes_no(f"[{UIColors.HIGHLIGHT}]Use DATABASE_URL? (Y/n):[/{UIColors.HIGHLIGHT}]") == "y" -======= - choice = CharReader.get_yes_no( - f"[{UIColors.HIGHLIGHT}]Use DATABASE_URL? (Y/n):[/{UIColors.HIGHLIGHT}]", default="y" + + def _get_structure_metadata( + self, project_dir: str, predefined: bool = False, unified: bool = False + ) -> Tuple[str, str, list[str], dict]: + """Helper method to get metadata for predefined/unified structures.""" + project_name = project_dir + app_names: list[str] = [] + + # Ask for workflows so shared templates for CI/CD can be added + use_github, use_gitlab = self.get_cicd_choice() + + # Ask database configuration + use_database_url = self.get_database_config_choice() + + # Default package_name to "backend" if project_dir is "." or empty + package_name = get_package_name(project_dir) + + project_module_name = "config" if predefined else "core" if unified else None + + metadata = ProjectMetadata( + package_name=package_name, + use_github_actions=use_github, + use_gitlab_ci=use_gitlab, + nested_apps=True, + nested_dir="apps", + use_database_url=use_database_url, + predefined_structure=predefined, + unified_structure=unified, + project_module_name=project_module_name, ) - # Return True if choice is 'y', False if 'n' - return choice == "y" ->>>>>>> origin/main + return project_name, "", app_names, metadata.to_dict() class CharReader: @@ -290,7 +292,6 @@ class CharReader: return CharReader._get_char_unix() @staticmethod -<<<<<<< HEAD def _is_enter_key(response: str) -> bool: """Check if response represents Enter key or control character.""" return not response or response in ("\r", "\n") or ord(response) < 32 @@ -323,30 +324,10 @@ class CharReader: # Handle Enter key (default) if CharReader._is_enter_key(response): return CharReader._handle_default(default) -======= - def get_yes_no(prompt: str, default: str = "n") -> str: - max_attempts = 3 - attempt = 0 - - while attempt < max_attempts: - try: - console.print(f"{prompt} ", end="") - sys.stdout.flush() - - reader = CharReader() - response = reader.get_char() - - # Handle Enter key (default) - in raw mode, Enter is '\r' (carriage return) - # Also handle empty string or other control characters as Enter - if not response or response == "\r" or response == "\n" or ord(response) < 32: - console.print(default.upper()) - return default.lower() ->>>>>>> origin/main response = response.lower() # Validate response -<<<<<<< HEAD if response in valid_keys_lower: console.print(response.upper()) return response @@ -359,30 +340,11 @@ class CharReader: if attempt < CharReader.MAX_ATTEMPTS: console.print( f"[{UIColors.MUTED}]Please try again ({attempt}/{CharReader.MAX_ATTEMPTS}).[/{UIColors.MUTED}]" -======= - if response in ("y", "n"): - console.print(response.upper()) - return response - - # Invalid input - show error (only show if it's a printable character) - console.print() - if response.isprintable(): - UIFormatter.print_error(f"Invalid input '{response}'. Please enter 'y' or 'n'.") - attempt += 1 - if attempt < max_attempts: - console.print( - f"[{UIColors.MUTED}]Please try again ({attempt}/{max_attempts}).[/{UIColors.MUTED}]" ->>>>>>> origin/main ) console.print() else: # Control character entered, treat as Enter -<<<<<<< HEAD return CharReader._handle_default(default) -======= - console.print(default.upper()) - return default.lower() ->>>>>>> origin/main except KeyboardInterrupt: console.print() @@ -395,7 +357,6 @@ class CharReader: return default.lower() @staticmethod -<<<<<<< HEAD def get_yes_no(prompt: str, default: str = None) -> str: # Auto-detect default from prompt format (capital letter indicates default) if default is None: @@ -423,70 +384,16 @@ class CharReader: default=default, error_message=f"Invalid input '{{response}}'. Please enter one of: {valid_keys_str}.", ) -======= - def get_cicd_choice(prompt: str, valid_keys: list[str], default: str = "n") -> str: - """ - Get CI/CD choice input with validation and retries. - Args: - prompt: The prompt message to display - valid_keys: List of valid key characters (e.g., ['g', 'l', 'b', 'n']) - default: Default value if Enter is pressed - - Returns: - Valid key (lowercase) - """ - max_attempts = 3 - attempt = 0 - valid_keys_lower = [k.lower() for k in valid_keys] - - while attempt < max_attempts: - try: - console.print(f"{prompt} ", end="") - sys.stdout.flush() - - reader = CharReader() - response = reader.get_char() - - # Handle Enter key (default) - in raw mode, Enter is '\r' (carriage return) - # Also handle empty string or other control characters as Enter - if not response or response == "\r" or response == "\n" or ord(response) < 32: - console.print(default.upper()) - return default.lower() - - response = response.lower() - - # Validate response - if response in valid_keys_lower: - console.print(response.upper()) - return response - - # Invalid input - show error (only show if it's a printable character) - console.print() - if response.isprintable(): - valid_keys_str = ", ".join(f"'{k}'" for k in valid_keys) - UIFormatter.print_error(f"Invalid input '{response}'. Please enter one of: {valid_keys_str}.") - attempt += 1 - if attempt < max_attempts: - console.print( - f"[{UIColors.MUTED}]Please try again ({attempt}/{max_attempts}).[/{UIColors.MUTED}]" - ) - console.print() - else: - # Control character entered, treat as Enter - console.print(default.upper()) - return default.lower() - - except KeyboardInterrupt: - console.print() - UIFormatter.print_info("\nOperation cancelled by user.") - raise - - # Max attempts reached - use default - console.print() - UIFormatter.print_warning(f"Maximum attempts reached. Using default: '{default.upper()}'.") - return default.lower() ->>>>>>> origin/main + @staticmethod + def get_structure_choice() -> str: + """Get structure type choice without requiring Enter key.""" + return CharReader._get_validated_char_input( + prompt=f"[{UIColors.HIGHLIGHT}]Choose structure type (1/2/3) [default: 1]:[/{UIColors.HIGHLIGHT}]", + valid_keys=["1", "2", "3"], + default="1", + error_message="Invalid input '{response}'. Please enter '1', '2', or '3'.", + ) @staticmethod def _get_char_windows() -> str: @@ -497,13 +404,32 @@ class CharReader: for encoding in encodings: try: ch = msvcrt.getch() - return ch.decode(encoding).lower() + # Detect Ctrl+C (ETX character \x03) + if isinstance(ch, bytes): + if ch == b"\x03": + raise KeyboardInterrupt + decoded = ch.decode(encoding).lower() + else: + if ord(ch) == 3: # Ctrl+C + raise KeyboardInterrupt + decoded = ch.lower() + return decoded except (UnicodeDecodeError, AttributeError): continue # Fallback try: - return msvcrt.getch().lower() + ch = msvcrt.getch() + if isinstance(ch, bytes): + if ch == b"\x03": + raise KeyboardInterrupt + return ch.decode("utf-8", errors="ignore").lower() + else: + if ord(ch) == 3: # Ctrl+C + raise KeyboardInterrupt + return ch.lower() + except KeyboardInterrupt: + raise except Exception: return "" @@ -514,201 +440,101 @@ class CharReader: try: tty.setraw(sys.stdin.fileno()) ch = sys.stdin.read(1) + # Detect Ctrl+C (ETX character \x03) + if ord(ch) == 3: # Ctrl+C + raise KeyboardInterrupt return ch.lower() finally: termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) def get_user_input() -> Tuple[str, str, str, list, dict]: - collector = InputCollector() - console.print() + try: + collector = InputCollector() + console.print() - # Pre-step: Ask for structure type first - UIFormatter.print_separator() -<<<<<<< HEAD - console.print(f"\n[{UIColors.INFO}]Choose Structure Type[/{UIColors.INFO}]\n") - console.print(f"[{UIColors.MUTED}]1. Standard structure (default Django layout)[/{UIColors.MUTED}]") - console.print(f"[{UIColors.MUTED}]2. Predefined structure (apps/users, apps/core, api/)[/{UIColors.MUTED}]") - console.print(f"[{UIColors.MUTED}]3. Unified structure (core/, apps/core, apps/api)[/{UIColors.MUTED}]") -======= - console.print(f"\n[{UIColors.INFO}]Step 1: Project Setup[/{UIColors.INFO}]\n") + # Pre-step: Ask for structure type first + UIFormatter.print_separator() + console.print(f"\n[{UIColors.INFO}]Choose Structure Type[/{UIColors.INFO}]\n") + console.print(f"[{UIColors.MUTED}]1. Standard structure (default Django layout)[/{UIColors.MUTED}]") + console.print(f"[{UIColors.MUTED}]2. Predefined structure (apps/users, apps/core, api/)[/{UIColors.MUTED}]") + console.print(f"[{UIColors.MUTED}]3. Unified structure (core/, apps/core, apps/api)[/{UIColors.MUTED}]") + console.print() - console.print(f"[{UIColors.HIGHLIGHT}]Project Directory[/{UIColors.HIGHLIGHT}]") - console.print(f"[{UIColors.MUTED}]Where your project files will be created[/{UIColors.MUTED}]") - console.print(f"[{UIColors.MUTED}]Press Enter or enter '.' to create in current directory[/{UIColors.MUTED}]") + structure_choice = CharReader.get_structure_choice() - # Get project directory with special handling for '.' and empty input - attempt = 0 - project_dir = None + use_predefined = structure_choice == "2" + use_unified = structure_choice == "3" - while attempt < collector.MAX_ATTEMPTS and project_dir is None: - try: - user_input = console.input( - f"[{UIColors.HIGHLIGHT}]Enter project directory name:[/{UIColors.HIGHLIGHT}] " - ).strip() + if use_predefined or use_unified: + UIFormatter.print_separator() + console.print(f"\n[{UIColors.INFO}]Step 1: Project Setup[/{UIColors.INFO}]\n") + console.print( + f"[{UIColors.MUTED}]Press Enter or enter '.' to create in current directory[/{UIColors.MUTED}]" + ) + project_dir = collector._get_project_directory() + if project_dir is None: + UIFormatter.print_error("Maximum attempts reached for project directory name. Exiting.") + sys.exit(1) - # Handle empty input or '.' - use current directory - if not user_input or user_input == ".": - project_dir = "." - UIFormatter.print_info(f"Creating project in current directory: {os.getcwd()}") - break + project_name, primary_app, app_names, metadata_dict = collector._get_structure_metadata( + project_dir, predefined=use_predefined, unified=use_unified + ) - # Validate the input - is_valid, error_msg = validate_project_name(user_input) - if not is_valid: - UIFormatter.print_error(error_msg) - attempt += 1 - collector._show_retry_message(attempt) - continue + return project_dir, project_name, primary_app, app_names, metadata_dict - # Check if directory exists and ask for alternative - if os.path.exists(user_input): - UIFormatter.print_error(f"Directory '{user_input}' already exists.") - UIFormatter.print_info( - "Please choose a different project directory name (or press Enter for current directory)." - ) - attempt += 1 - collector._show_retry_message(attempt) - continue - - project_dir = user_input - break - - except KeyboardInterrupt: - UIFormatter.print_info("\nSetup cancelled by user.") - sys.exit(0) - - if project_dir is None: - UIFormatter.print_error("Maximum attempts reached for project directory name. Exiting.") - sys.exit(1) - ->>>>>>> origin/main - console.print() - - structure_choice = console.input( - f"[{UIColors.HIGHLIGHT}]Choose structure type (1/2/3) [default: 1]:[/{UIColors.HIGHLIGHT}] " - ).strip() - if not structure_choice: - structure_choice = "1" - - use_predefined = structure_choice == "2" - use_unified = structure_choice == "3" - - if use_predefined: + # Section 1: Project Setup (standard flow) UIFormatter.print_separator() console.print(f"\n[{UIColors.INFO}]Step 1: Project Setup[/{UIColors.INFO}]\n") console.print(f"[{UIColors.MUTED}]Press Enter or enter '.' to create in current directory[/{UIColors.MUTED}]") + project_dir = collector._get_project_directory() + if project_dir is None: UIFormatter.print_error("Maximum attempts reached for project directory name. Exiting.") sys.exit(1) - # Minimal inputs: project_dir only, then ask for CI/CD choice. - project_name = project_dir - app_names: list[str] = [] + console.print() - # Ask for workflows so shared templates for CI/CD can be added - use_github, use_gitlab = collector.get_cicd_choice() - - # Ask database configuration for predefined structure as well - use_database_url = collector.get_database_config_choice() - # Default package_name to "backend" if project_dir is "." or empty - package_name = "backend" if project_dir == "." or not project_dir else project_dir - metadata = ProjectMetadata( - package_name=package_name, - use_github_actions=use_github, - use_gitlab_ci=use_gitlab, - nested_apps=True, - nested_dir="apps", - use_database_url=use_database_url, - predefined_structure=True, - project_module_name="config", + console.print(f"[{UIColors.MUTED}]Common names: config, core, settings[/{UIColors.MUTED}]") + project_name = collector.get_validated_input( + "Enter Django project name", validate_project_name, "Django project name" ) + console.print() - return project_dir, project_name, "", app_names, metadata.to_dict() - - if use_unified: + # Section 2: Django Apps + console.print() UIFormatter.print_separator() - console.print(f"\n[{UIColors.INFO}]Step 1: Project Setup[/{UIColors.INFO}]\n") - console.print(f"[{UIColors.MUTED}]Press Enter or enter '.' to create in current directory[/{UIColors.MUTED}]") - project_dir = collector._get_project_directory() - if project_dir is None: - UIFormatter.print_error("Maximum attempts reached for project directory name. Exiting.") - sys.exit(1) + console.print(f"\n[{UIColors.INFO}]Step 2: Django Apps[/{UIColors.INFO}]\n") + nested, nested_dir = collector.get_nested_apps_config() + app_names = collector.get_app_names() + console.print() - # Minimal inputs: project_dir only, then ask for CI/CD choice. - project_name = project_dir - app_names: list[str] = [] - - # Ask for workflows so shared templates for CI/CD can be added + # Section 3: CI/CD Pipeline use_github, use_gitlab = collector.get_cicd_choice() - # Ask database configuration for unified structure as well + # Section 4: Database Configuration use_database_url = collector.get_database_config_choice() + + # Create metadata # Default package_name to "backend" if project_dir is "." or empty - package_name = "backend" if project_dir == "." or not project_dir else project_dir + package_name = get_package_name(project_dir) metadata = ProjectMetadata( package_name=package_name, use_github_actions=use_github, use_gitlab_ci=use_gitlab, - nested_apps=True, - nested_dir="apps", + nested_apps=nested, + nested_dir=nested_dir, use_database_url=use_database_url, - unified_structure=True, - project_module_name="core", ) - return project_dir, project_name, "", app_names, metadata.to_dict() + console.print() - # Section 1: Project Setup (standard flow) - UIFormatter.print_separator() - console.print(f"\n[{UIColors.INFO}]Step 1: Project Setup[/{UIColors.INFO}]\n") - console.print(f"[{UIColors.MUTED}]Press Enter or enter '.' to create in current directory[/{UIColors.MUTED}]") - - project_dir = collector._get_project_directory() - - if project_dir is None: - UIFormatter.print_error("Maximum attempts reached for project directory name. Exiting.") - sys.exit(1) - - console.print() - - console.print(f"[{UIColors.MUTED}]Common names: config, core, settings[/{UIColors.MUTED}]") - project_name = collector.get_validated_input( - "Enter Django project name", validate_project_name, "Django project name" - ) - console.print() - - # Section 2: Django Apps - console.print() - UIFormatter.print_separator() - console.print(f"\n[{UIColors.INFO}]Step 2: Django Apps[/{UIColors.INFO}]\n") - nested, nested_dir = collector.get_nested_apps_config() - app_names = collector.get_app_names() - console.print() - - # Section 3: CI/CD Pipeline - use_github, use_gitlab = collector.get_cicd_choice() - - # Section 4: Database Configuration - use_database_url = collector.get_database_config_choice() - - # Create metadata - # Default package_name to "backend" if project_dir is "." or empty - package_name = "backend" if project_dir == "." or not project_dir else project_dir - metadata = ProjectMetadata( - package_name=package_name, - use_github_actions=use_github, - use_gitlab_ci=use_gitlab, - nested_apps=nested, - nested_dir=nested_dir, - use_database_url=use_database_url, - ) - - console.print() - - # Return in legacy format for backward compatibility - return project_dir, project_name, app_names[0], app_names, metadata.to_dict() + # Return in legacy format for backward compatibility + return project_dir, project_name, app_names[0], app_names, metadata.to_dict() + except KeyboardInterrupt: + UIFormatter.print_info("\nSetup cancelled by user.") + sys.exit(0) def confirm_setup(project_dir: str, project_name: str, app_names: list, metadata: dict) -> bool: @@ -734,13 +560,7 @@ def confirm_setup(project_dir: str, project_name: str, app_names: list, metadata console.print() try: -<<<<<<< HEAD response = CharReader.get_yes_no(f"[{UIColors.WARNING}]Proceed with setup? (y/N):[/{UIColors.WARNING}]") -======= - response = CharReader.get_yes_no( - f"[{UIColors.WARNING}]Proceed with setup? (y/N):[/{UIColors.WARNING}]", default="n" - ) ->>>>>>> origin/main return response == "y" except KeyboardInterrupt: diff --git a/djinit/scripts/project.py b/djinit/scripts/project.py index da7a9a2..bddfb99 100644 --- a/djinit/scripts/project.py +++ b/djinit/scripts/project.py @@ -9,6 +9,7 @@ from djinit.scripts.console_ui import UIFormatter from djinit.scripts.django_helper import DjangoHelper from djinit.utils import ( calculate_app_module_paths, + create_directory_with_init, extract_existing_apps, get_base_settings_path, insert_apps_into_user_defined_apps, @@ -22,23 +23,14 @@ class ProjectManager: self.project_name = project_name self.app_names = app_names self.metadata = metadata -<<<<<<< HEAD self.project_root = os.getcwd() if project_dir == "." else os.path.join(os.getcwd(), project_dir) # Support custom module name for the Django config package (e.g., 'config') # Fall back to project_name if key present but None self.module_name = metadata.get("project_module_name") or self.project_name -======= - # Handle '.' for current directory - if project_dir == ".": - self.project_root = os.getcwd() - else: - self.project_root = os.path.join(os.getcwd(), project_dir) ->>>>>>> origin/main def create_project(self) -> bool: os.makedirs(self.project_root, exist_ok=True) -<<<<<<< HEAD unified = self.metadata.get("unified_structure", False) success = DjangoHelper.startproject(self.module_name, self.project_root, unified=unified) if success: @@ -46,29 +38,19 @@ class ProjectManager: else: UIFormatter.print_error(f"Failed to create Django project '{self.project_name}'") return success -======= - # Create Django project inside the directory - # Use 'python -m django' instead of 'django-admin' for better compatibility - subprocess.run( - [sys.executable, "-m", "django", "startproject", self.project_name, self.project_root], - check=True, - ) - UIFormatter.print_success(f"Django project '{self.project_name}' created successfully!") - return True ->>>>>>> origin/main + def _get_apps_base_dir(self) -> str: + """Get the base directory for apps, considering nested structure.""" + apps_base_dir = self.project_root + if self.metadata.get("nested_apps") and self.metadata.get("nested_dir"): + apps_base_dir = os.path.join(self.project_root, self.metadata.get("nested_dir")) + return apps_base_dir def create_apps(self) -> bool: - apps_base_dir = self.project_root - nested = bool(self.metadata.get("nested_apps")) - nested_dir_name = self.metadata.get("nested_dir") if nested else None + apps_base_dir = self._get_apps_base_dir() - if nested and nested_dir_name: - apps_base_dir = os.path.join(self.project_root, nested_dir_name) - os.makedirs(apps_base_dir, exist_ok=True) - init_file = os.path.join(apps_base_dir, "__init__.py") - if not os.path.exists(init_file): - open(init_file, "a").close() + if apps_base_dir != self.project_root: + create_directory_with_init(apps_base_dir, f"Created {os.path.basename(apps_base_dir)}/__init__.py") for app_name in self.app_names: success = DjangoHelper.startapp(app_name, apps_base_dir) @@ -78,12 +60,12 @@ class ProjectManager: UIFormatter.print_success(f"Django app '{app_name}' created successfully!") # Add all apps to USER_DEFINED_APPS in settings - if not self._add_apps_to_settings(): + if not self.add_apps_to_settings(): return False return True - def _add_apps_to_settings(self) -> bool: + def add_apps_to_settings(self) -> bool: """Add all apps to USER_DEFINED_APPS in base.py settings file.""" base_settings_path = get_base_settings_path(self.project_root, self.module_name) @@ -116,10 +98,6 @@ class ProjectManager: UIFormatter.print_success(f"Added apps to USER_DEFINED_APPS: {added_apps_str}") return True - # Public wrapper to allow external callers to add apps based on current state - def add_apps_to_settings(self) -> bool: - return self._add_apps_to_settings() - def validate_project_structure(self) -> bool: required_files = [ os.path.join(self.project_root, "manage.py"), @@ -133,9 +111,7 @@ class ProjectManager: os.path.join(self.project_root, self.module_name, "asgi.py"), ] - apps_base_dir = self.project_root - if self.metadata.get("nested_apps") and self.metadata.get("nested_dir"): - apps_base_dir = os.path.join(self.project_root, self.metadata.get("nested_dir")) + apps_base_dir = self._get_apps_base_dir() # In predefined or unified structure, apps layout differs; skip strict per-app validation if not self.metadata.get("predefined_structure") and not self.metadata.get("unified_structure"): diff --git a/djinit/templates/predefined/api/urls.j2 b/djinit/templates/predefined/api/urls.j2 index 69585ec..51e3dfb 100644 --- a/djinit/templates/predefined/api/urls.j2 +++ b/djinit/templates/predefined/api/urls.j2 @@ -5,5 +5,3 @@ app_name = "api" urlpatterns = [ path("v1/", include("api.v1.urls")), ] - - diff --git a/djinit/templates/predefined/api/v1/urls.j2 b/djinit/templates/predefined/api/v1/urls.j2 index 40ebdea..7a0d9a8 100644 --- a/djinit/templates/predefined/api/v1/urls.j2 +++ b/djinit/templates/predefined/api/v1/urls.j2 @@ -5,5 +5,3 @@ app_name = "v1" urlpatterns = [ path("users/", include("apps.users.urls")), ] - - diff --git a/djinit/templates/predefined/apps/generic/models.j2 b/djinit/templates/predefined/apps/generic/models.j2 index 8067069..9f99a46 100644 --- a/djinit/templates/predefined/apps/generic/models.j2 +++ b/djinit/templates/predefined/apps/generic/models.j2 @@ -6,5 +6,3 @@ class {{ model_class_name }}(models.Model): # Add fields here pass - - diff --git a/djinit/templates/predefined/apps/generic/serializers.j2 b/djinit/templates/predefined/apps/generic/serializers.j2 index ef60bcb..86f1b45 100644 --- a/djinit/templates/predefined/apps/generic/serializers.j2 +++ b/djinit/templates/predefined/apps/generic/serializers.j2 @@ -7,5 +7,3 @@ class {{ model_class_name }}Serializer(serializers.ModelSerializer): class Meta: model = {{ model_class_name }} fields = "__all__" - - diff --git a/djinit/templates/predefined/apps/generic/services.j2 b/djinit/templates/predefined/apps/generic/services.j2 index eb75f90..2203fb8 100644 --- a/djinit/templates/predefined/apps/generic/services.j2 +++ b/djinit/templates/predefined/apps/generic/services.j2 @@ -8,5 +8,3 @@ def get_{{ app_name }}_by_id(object_id: int) -> Optional[{{ model_class_name }}] return {{ model_class_name }}.objects.get(id=object_id) except {{ model_class_name }}.DoesNotExist: return None - - diff --git a/djinit/templates/predefined/apps/generic/tests.j2 b/djinit/templates/predefined/apps/generic/tests.j2 index 910bad8..41fc906 100644 --- a/djinit/templates/predefined/apps/generic/tests.j2 +++ b/djinit/templates/predefined/apps/generic/tests.j2 @@ -10,5 +10,3 @@ def test_{{ app_name }}_list_empty(): response = client.get(url) assert response.status_code == 200 assert response.json() == [] - - diff --git a/djinit/templates/predefined/apps/generic/urls.j2 b/djinit/templates/predefined/apps/generic/urls.j2 index 68578e9..caec1a6 100644 --- a/djinit/templates/predefined/apps/generic/urls.j2 +++ b/djinit/templates/predefined/apps/generic/urls.j2 @@ -12,5 +12,3 @@ router.register(r"{{ app_name }}", {{ model_class_name }}ViewSet, basename="{{ a urlpatterns = [ path("", include(router.urls)), ] - - diff --git a/djinit/templates/predefined/apps/generic/views.j2 b/djinit/templates/predefined/apps/generic/views.j2 index af3545c..5976564 100644 --- a/djinit/templates/predefined/apps/generic/views.j2 +++ b/djinit/templates/predefined/apps/generic/views.j2 @@ -7,5 +7,3 @@ from {{ app_module }}.serializers.{{ app_name }}_serializer import {{ model_clas class {{ model_class_name }}ViewSet(viewsets.ModelViewSet): queryset = {{ model_class_name }}.objects.all() serializer_class = {{ model_class_name }}Serializer - - diff --git a/djinit/templates/predefined/apps/users/models.j2 b/djinit/templates/predefined/apps/users/models.j2 index 556619b..f6236bd 100644 --- a/djinit/templates/predefined/apps/users/models.j2 +++ b/djinit/templates/predefined/apps/users/models.j2 @@ -7,5 +7,3 @@ class User(AbstractUser): # Add custom fields here if needed pass - - diff --git a/djinit/templates/predefined/apps/users/serializers.j2 b/djinit/templates/predefined/apps/users/serializers.j2 index ca7a68e..d7a4f40 100644 --- a/djinit/templates/predefined/apps/users/serializers.j2 +++ b/djinit/templates/predefined/apps/users/serializers.j2 @@ -7,5 +7,3 @@ class UserSerializer(serializers.ModelSerializer): class Meta: model = User fields = ["id", "username", "email", "first_name", "last_name"] - - diff --git a/djinit/templates/predefined/apps/users/services.j2 b/djinit/templates/predefined/apps/users/services.j2 index 5888378..036060b 100644 --- a/djinit/templates/predefined/apps/users/services.j2 +++ b/djinit/templates/predefined/apps/users/services.j2 @@ -8,5 +8,3 @@ def get_user_by_id(user_id: int) -> Optional[User]: return User.objects.get(id=user_id) except User.DoesNotExist: return None - - diff --git a/djinit/templates/predefined/apps/users/tests.j2 b/djinit/templates/predefined/apps/users/tests.j2 index 3bdb897..1bde766 100644 --- a/djinit/templates/predefined/apps/users/tests.j2 +++ b/djinit/templates/predefined/apps/users/tests.j2 @@ -10,5 +10,3 @@ def test_users_list_empty(): response = client.get(url) assert response.status_code == 200 assert response.json() == [] - - diff --git a/djinit/templates/predefined/apps/users/urls.j2 b/djinit/templates/predefined/apps/users/urls.j2 index 98e7d7a..08a6fb4 100644 --- a/djinit/templates/predefined/apps/users/urls.j2 +++ b/djinit/templates/predefined/apps/users/urls.j2 @@ -12,5 +12,3 @@ router.register(r"users", UserViewSet, basename="users") urlpatterns = [ path("", include(router.urls)), ] - - diff --git a/djinit/templates/predefined/apps/users/views.j2 b/djinit/templates/predefined/apps/users/views.j2 index 660fcc7..8db2347 100644 --- a/djinit/templates/predefined/apps/users/views.j2 +++ b/djinit/templates/predefined/apps/users/views.j2 @@ -11,5 +11,3 @@ class UserViewSet(viewsets.ReadOnlyModelViewSet): def list(self, request, *args, **kwargs): return super().list(request, *args, **kwargs) - - diff --git a/djinit/templates/predefined/core/exceptions.j2 b/djinit/templates/predefined/core/exceptions.j2 index 3a2f7fa..bb426d3 100644 --- a/djinit/templates/predefined/core/exceptions.j2 +++ b/djinit/templates/predefined/core/exceptions.j2 @@ -3,5 +3,3 @@ class ApplicationError(Exception): def __init__(self, message: str): super().__init__(message) - - diff --git a/djinit/templates/predefined/core/middleware/request_logger.j2 b/djinit/templates/predefined/core/middleware/request_logger.j2 index 00ed0fe..d922ad1 100644 --- a/djinit/templates/predefined/core/middleware/request_logger.j2 +++ b/djinit/templates/predefined/core/middleware/request_logger.j2 @@ -12,5 +12,3 @@ class RequestLoggerMiddleware: logger.info("%s %s", request.method, request.path) response = self.get_response(request) return response - - diff --git a/djinit/templates/predefined/core/mixins/timestamped_model.j2 b/djinit/templates/predefined/core/mixins/timestamped_model.j2 index 3d27df1..6b2e70e 100644 --- a/djinit/templates/predefined/core/mixins/timestamped_model.j2 +++ b/djinit/templates/predefined/core/mixins/timestamped_model.j2 @@ -7,5 +7,3 @@ class TimestampedModel(models.Model): class Meta: abstract = True - - diff --git a/djinit/templates/predefined/core/utils/responses.j2 b/djinit/templates/predefined/core/utils/responses.j2 index fc6bf7a..bae79a1 100644 --- a/djinit/templates/predefined/core/utils/responses.j2 +++ b/djinit/templates/predefined/core/utils/responses.j2 @@ -7,5 +7,3 @@ def success(data=None, status_code=200): def error(message, status_code=400): return Response({"success": False, "message": message}, status=status_code) - - diff --git a/djinit/templates/project/urls_with_api.j2 b/djinit/templates/project/urls_with_api.j2 index e236407..ee6843f 100644 --- a/djinit/templates/project/urls_with_api.j2 +++ b/djinit/templates/project/urls_with_api.j2 @@ -26,4 +26,4 @@ if settings.DEBUG: urlpatterns += [ path("schema/", SpectacularAPIView.as_view(), name="schema"), path("docs/", SpectacularSwaggerView.as_view(url_name="schema")), - ] \ No newline at end of file + ] diff --git a/djinit/templates/unified/apps/core/apps.j2 b/djinit/templates/unified/apps/core/apps.j2 index e59efb0..1d202ca 100644 --- a/djinit/templates/unified/apps/core/apps.j2 +++ b/djinit/templates/unified/apps/core/apps.j2 @@ -6,4 +6,3 @@ from django.apps import AppConfig class CoreConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "apps.core" - diff --git a/djinit/templates/unified/apps/core/models/__init__.py.j2 b/djinit/templates/unified/apps/core/models/__init__.py.j2 index 71067ec..89b2eb6 100644 --- a/djinit/templates/unified/apps/core/models/__init__.py.j2 +++ b/djinit/templates/unified/apps/core/models/__init__.py.j2 @@ -2,4 +2,3 @@ from .base import AbstractBaseModel from .mixins import TimestampedModel __all__ = ["AbstractBaseModel", "TimestampedModel"] - diff --git a/djinit/templates/unified/apps/core/models/base.py.j2 b/djinit/templates/unified/apps/core/models/base.py.j2 index 95ed131..a3970f9 100644 --- a/djinit/templates/unified/apps/core/models/base.py.j2 +++ b/djinit/templates/unified/apps/core/models/base.py.j2 @@ -11,4 +11,3 @@ class AbstractBaseModel(models.Model): class Meta: abstract = True - diff --git a/djinit/templates/unified/apps/core/models/mixins.py.j2 b/djinit/templates/unified/apps/core/models/mixins.py.j2 index c456739..fe40544 100644 --- a/djinit/templates/unified/apps/core/models/mixins.py.j2 +++ b/djinit/templates/unified/apps/core/models/mixins.py.j2 @@ -11,4 +11,3 @@ class TimestampedModel(models.Model): class Meta: abstract = True - diff --git a/djinit/templates/unified/apps/core/utils/__init__.py.j2 b/djinit/templates/unified/apps/core/utils/__init__.py.j2 index 56fdd17..155f5b4 100644 --- a/djinit/templates/unified/apps/core/utils/__init__.py.j2 +++ b/djinit/templates/unified/apps/core/utils/__init__.py.j2 @@ -1,4 +1,3 @@ from .responses import success, error __all__ = ["success", "error"] - diff --git a/djinit/templates/unified/apps/core/utils/responses.py.j2 b/djinit/templates/unified/apps/core/utils/responses.py.j2 index 7f32fda..929c2f6 100644 --- a/djinit/templates/unified/apps/core/utils/responses.py.j2 +++ b/djinit/templates/unified/apps/core/utils/responses.py.j2 @@ -11,4 +11,3 @@ def success(data=None, status_code=200): def error(message, status_code=400): """Return an error response.""" return Response({"success": False, "message": message}, status=status_code) - diff --git a/djinit/utils.py b/djinit/utils.py index a41e9a0..a11cdc5 100644 --- a/djinit/utils.py +++ b/djinit/utils.py @@ -14,10 +14,6 @@ from djinit.scripts.template_engine import template_engine def format_file(filename: str) -> None: """Format Python file using Ruff formatter.""" -<<<<<<< HEAD -======= - # Use 'python -m ruff' instead of 'ruff' for better compatibility ->>>>>>> origin/main subprocess.run([sys.executable, "-m", "ruff", "format", filename], check=False, capture_output=True) @@ -51,6 +47,38 @@ def create_init_file(directory: str, success_message: str) -> None: create_file_from_template(init_path, "shared/init.j2", {}, success_message, should_format=False) +def create_directory_with_init(dir_path: str, message: str) -> None: + """Create directory and __init__.py file.""" + os.makedirs(dir_path, exist_ok=True) + create_init_file(dir_path, message) + + +def create_files_from_templates(base_dir: str, files: list, prefix: str = "", should_format: bool = False) -> None: + """Create multiple files from templates. + + Args: + base_dir: Base directory where files will be created + files: List of tuples (filename, template_path, context) + prefix: Optional prefix for success message + should_format: Whether to format the created files + """ + for file_spec in files: + if len(file_spec) == 3: + filename, template_path, context = file_spec + else: + # Support (filename, template_path) format with empty context + filename, template_path = file_spec + context = {} + filepath = os.path.join(base_dir, filename) + message = f"Created {prefix}{filename}" if prefix else f"Created {filename}" + create_file_from_template(filepath, template_path, context, message, should_format=should_format) + + +def get_package_name(project_dir: str) -> str: + """Get package name, defaulting to 'backend' if project_dir is '.' or empty.""" + return "backend" if project_dir == "." or not project_dir else project_dir + + # change the current working dir tempporary @contextmanager def change_cwd(path: str): @@ -62,24 +90,41 @@ def change_cwd(path: str): os.chdir(original_cwd) -# get app names from USER_DEFINED_APPS section in project_name/settings/base.py -def extract_existing_apps(content: str) -> set: +def _parse_app_entry_from_line(line: str) -> str | None: + """Extract app name from a line in USER_DEFINED_APPS section.""" + line_stripped = line.strip().strip(",").strip() + for quote in ('"', "'"): + if line_stripped.startswith(quote) and line_stripped.endswith(quote): + return line_stripped.strip(quote) + return None + + +def _is_user_apps_section_start(line: str) -> bool: + """Check if line starts USER_DEFINED_APPS section.""" + return "USER_DEFINED_APPS" in line and "=" in line + + +def _iterate_user_apps_lines(content: str): + """Generator that yields lines within USER_DEFINED_APPS section.""" lines = content.split("\n") - existing_apps = set() in_user_apps = False for line in lines: - if "USER_DEFINED_APPS" in line and "=" in line: + if _is_user_apps_section_start(line): in_user_apps = True elif in_user_apps and line.strip() == "]": break elif in_user_apps: - line_stripped = line.strip().strip(",").strip() - for quote in ('"', "'"): - if line_stripped.startswith(quote) and line_stripped.endswith(quote): - existing_apps.add(line_stripped.strip(quote)) - break + yield line + +# get app names from USER_DEFINED_APPS section in project_name/settings/base.py +def extract_existing_apps(content: str) -> set: + existing_apps = set() + for line in _iterate_user_apps_lines(content): + app_name = _parse_app_entry_from_line(line) + if app_name: + existing_apps.add(app_name) return existing_apps @@ -123,7 +168,7 @@ def insert_apps_into_user_defined_apps(content: str, apps_to_add: list) -> str: for i, line in enumerate(lines): # Entering USER_DEFINED_APPS section - if "USER_DEFINED_APPS" in line and "=" in line and not apps_added: + if _is_user_apps_section_start(line) and not apps_added: in_user_apps = True # Handle closing bracket on same line @@ -166,7 +211,7 @@ def insert_apps_into_user_defined_apps(content: str, apps_to_add: list) -> str: def calculate_app_module_paths(app_names: list, metadata: dict) -> list: nested = bool(metadata.get("nested_apps")) nested_dir_name = metadata.get("nested_dir") if nested else None - return [f"{nested_dir_name}.{app_name}" if nested and nested_dir_name else app_name for app_name in app_names] + return [calculate_app_module_path(app_name, nested, nested_dir_name) for app_name in app_names] # calculate app module path for a single app @@ -234,22 +279,13 @@ def detect_nested_structure_from_settings( with open(base_settings_path) as f: base_content = f.read() - in_user_apps = False - for line in base_content.splitlines(): - if "USER_DEFINED_APPS" in line and "=" in line: - in_user_apps = True - continue - if in_user_apps and "]" in line: - break - if in_user_apps: - line_stripped = line.strip().strip(",") - if line_stripped.startswith('"') or line_stripped.startswith("'"): - app_label = line_stripped.strip("\"'") - if "." in app_label: - nested_dir_name = app_label.split(".", 1)[0] - nested_path = os.path.join(search_dir, nested_dir_name) - if os.path.isdir(nested_path): - return True, nested_dir_name, nested_path + for line in _iterate_user_apps_lines(base_content): + app_label = _parse_app_entry_from_line(line) + if app_label and "." in app_label: + nested_dir_name = app_label.split(".", 1)[0] + nested_path = os.path.join(search_dir, nested_dir_name) + if os.path.isdir(nested_path): + return True, nested_dir_name, nested_path except Exception: pass