diff --git a/djinit/cli.py b/djinit/cli.py index 11f8dc5..3509e29 100644 --- a/djinit/cli.py +++ b/djinit/cli.py @@ -19,7 +19,7 @@ class Cli: self.primary_app = primary_app self.app_names = app_names self.metadata = metadata - if project_dir == ".": # handle '.' for current directory + if project_dir == ".": self.project_root = os.getcwd() else: self.project_root = os.path.join(os.getcwd(), project_dir) @@ -28,36 +28,23 @@ class Cli: self.file_manager = FileManager(self.project_root, project_name, app_names, metadata) def run_setup(self) -> bool: - # If predefined structure is enabled, adjust metadata defaults if self.metadata.get("predefined_structure"): - # Default module name to 'config' for conventional layout self.metadata.setdefault("project_module_name", "config") - # Ensure nested apps live under 'apps' self.metadata.setdefault("nested_apps", True) self.metadata.setdefault("nested_dir", "apps") - # Default apps for predefined structure if not self.app_names: self.app_names = ["users", "core"] - # propagate to managers self.project_manager.app_names = self.app_names - # If unified structure is enabled, adjust metadata defaults if self.metadata.get("unified_structure"): - # For unified structure, module name is 'core' self.metadata.setdefault("project_module_name", "core") - # Ensure nested apps live under 'apps' self.metadata.setdefault("nested_apps", True) self.metadata.setdefault("nested_dir", "apps") - # Default apps for unified structure (empty, apps/core and apps/api are created by default) if not self.app_names: self.app_names = [] - # propagate to managers self.project_manager.app_names = self.app_names - # If single structure is enabled, adjust metadata defaults if self.metadata.get("single_structure"): - # Module name is already set in input_handler - # No nested apps self.metadata.setdefault("nested_apps", False) if not self.app_names: self.app_names = [] @@ -68,13 +55,10 @@ class Cli: steps.append(("Creating Django project", self.project_manager.create_project)) if self.metadata.get("unified_structure"): - # Build unified structure steps.append(("Creating unified structure", self.file_manager.create_unified_structure)) elif self.metadata.get("single_structure"): - # Build single folder structure steps.append(("Creating single folder structure", self.file_manager.create_single_structure)) elif self.metadata.get("predefined_structure"): - # Build custom tree and then inject apps into settings steps.append(("Creating predefined structure", self.file_manager.create_predefined_structure)) steps.append(("Adding apps to settings", self.project_manager.add_apps_to_settings)) else: @@ -95,11 +79,9 @@ class Cli: total_steps = len(steps) success = True - # Create live progress display progress, task = UIFormatter.create_live_progress(description="Setup Progress", total_steps=total_steps) with progress: - # Execute each step with progress tracking for step_number, (description, step_func) in enumerate(steps, 1): result = step_func() if not result: @@ -107,7 +89,6 @@ class Cli: UIFormatter.print_error(f"Step {step_number} failed: {description}") break - # Update the same progress bar progress.update(task, advance=1, description=f"Step {step_number}/{total_steps}") return success diff --git a/djinit/generator.py b/djinit/generator.py index f1e9754..50fb874 100644 --- a/djinit/generator.py +++ b/djinit/generator.py @@ -16,31 +16,26 @@ from djinit.scripts.input_handler import confirm_setup, get_user_input def clear_screen() -> None: try: if os.name == "nt": - subprocess.run("cls", shell=True, check=True) # Windows + subprocess.run("cls", shell=True, check=True) else: - subprocess.run("clear", shell=True, check=True) # Linux/MacOS + subprocess.run("clear", shell=True, check=True) except subprocess.CalledProcessError: - # If clearing fails, just print some newlines console.print("\n" * 50) def main() -> None: args = parse_arguments() - # Handle secret command if args.command == "secret": handle_secret_command(args) return - # Handle app command if args.command == "app": handle_app_command(args) return - # Default to setup command clear_screen() - # Display welcome screen console.print() console.print(UIFormatter.create_welcome_panel()) console.print() diff --git a/djinit/scripts/app_generator.py b/djinit/scripts/app_generator.py index e679d4f..f5b534f 100644 --- a/djinit/scripts/app_generator.py +++ b/djinit/scripts/app_generator.py @@ -69,16 +69,13 @@ class AppManager: return os.path.exists(app_path) def _create_django_app(self) -> bool: - # Verify manage.py exists in project root if not os.path.exists(self.manage_py_path): UIFormatter.print_error("Could not find manage.py file in project root") return False - # 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")) - # Otherwise, use standard flat app generation _, _, apps_base_dir = self._get_project_structure() success = DjangoHelper.startapp(self.app_name, apps_base_dir) if success: @@ -101,15 +98,12 @@ class AppManager: with open(base_settings_path) as f: content = f.read() - # Get the app's module path (e.g., "apps.users" or just "users") - # For predefined structure, use "apps" as nested_dir if self._is_predefined_structure(): app_module_path = f"apps.{self.app_name}" else: 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 existing_apps = extract_existing_apps(content) if app_module_path in existing_apps: UIFormatter.print_success(f"App '{app_module_path}' already configured in USER_DEFINED_APPS") @@ -119,7 +113,6 @@ class AppManager: UIFormatter.print_error("Could not find USER_DEFINED_APPS section in base.py") return False - # Add app to USER_DEFINED_APPS list in settings file updated_content = insert_apps_into_user_defined_apps(content, [app_module_path]) if not updated_content: return False @@ -133,33 +126,27 @@ class AppManager: 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: 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 apps_dir = os.path.join(self.current_dir, "apps") api_dir = os.path.join(self.current_dir, "api") return os.path.isdir(apps_dir) and os.path.isdir(api_dir) def _is_restricted_structure(self) -> bool: """Check if the project structure is Unified or Single Folder.""" - # Check for Unified Structure: apps/api exists apps_api_dir = os.path.join(self.current_dir, "apps", "api") if os.path.isdir(apps_api_dir): return True - # Check for Single Folder Structure - # No apps/ directory, but project dir has models/ and api/ apps_dir = os.path.join(self.current_dir, "apps") if os.path.isdir(apps_dir): return False @@ -179,12 +166,10 @@ class AppManager: os.makedirs(app_dir, exist_ok=True) create_init_file(app_dir, f"Created apps/{self.app_name}/__init__.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"), "components/apps.j2", @@ -192,7 +177,6 @@ class AppManager: f"Created apps/{self.app_name}/apps.py", ) - # Subfolders subfolders = { "models": [(f"{self.app_name}.py", "presets/predefined/apps/generic/models.j2")], "serializers": [(f"{self.app_name}_serializer.py", "presets/predefined/apps/generic/serializers.j2")], @@ -209,7 +193,6 @@ class AppManager: file_path, template, base_context, f"Created apps/{self.app_name}/{folder}/{filename}" ) - # urls.py at app root create_file_from_template( os.path.join(app_dir, "urls.py"), "presets/predefined/apps/generic/urls.j2", @@ -217,7 +200,6 @@ class AppManager: f"Created apps/{self.app_name}/urls.py", ) - # Add route include to api/v1/urls.py if present self._add_to_api_v1_urls(self.app_name) UIFormatter.print_success(f"Created Django app '{self.app_name}' with predefined structure in {apps_dir}") @@ -238,10 +220,8 @@ class AppManager: include_stmt = f'path("{app_name}/", include("apps.{app_name}.urls")),' if include_stmt in content: - # already present return - # Insert into urlpatterns list before closing ] if "urlpatterns = [" in content: parts = content.split("urlpatterns = [", 1) before = parts[0] @@ -249,7 +229,6 @@ class AppManager: if "]" in rest: idx = rest.rfind("]") new_rest = rest[:idx] - # ensure newline and indentation if not new_rest.endswith("\n"): new_rest += "\n" new_rest += f" {include_stmt}\n" + rest[idx:] @@ -258,5 +237,4 @@ class AppManager: with open(api_v1_urls, "w", encoding="utf-8") as f: f.write(content) except Exception: - # Non-fatal; leave API routes unchanged if edit fails return diff --git a/djinit/scripts/command_handler.py b/djinit/scripts/command_handler.py index ec03879..362a52b 100644 --- a/djinit/scripts/command_handler.py +++ b/djinit/scripts/command_handler.py @@ -20,17 +20,14 @@ def parse_arguments() -> argparse.Namespace: subparsers = parser.add_subparsers(dest="command", help="Available commands") - # Setup command (default) setup_parser = subparsers.add_parser("setup", help="Create a new Django project") setup_parser.add_argument("--project", help="Django project name") setup_parser.add_argument("--app", help="Django app name") - # Secret command secret_parser = subparsers.add_parser("secret", help="Generate Django secret keys") secret_parser.add_argument("--count", type=int, default=3, help="Number of keys to generate") secret_parser.add_argument("--length", type=int, default=50, help="Length of each secret key") - # App command app_parser = subparsers.add_parser("app", help="Create one or more Django apps (comma or space separated)") app_parser.add_argument( "app_name", @@ -70,8 +67,6 @@ def handle_app_command(args: argparse.Namespace) -> None: UIFormatter.print_header("Django App Creation") UIFormatter.print_info("") - # Parse comma-separated names and validate - # args.app_name is a list due to nargs="+"; split each token by comma tokens = args.app_name if isinstance(args.app_name, list) else [args.app_name] pieces = [] for token in tokens: @@ -83,12 +78,10 @@ def handle_app_command(args: argparse.Namespace) -> None: pieces.append(part) app_names = pieces - # If no comma, keep single name behavior if not app_names: UIFormatter.print_error("App name cannot be empty") sys.exit(1) - # Deduplicate while preserving order seen = set() deduped_names = [] for name in app_names: @@ -102,7 +95,6 @@ def handle_app_command(args: argparse.Namespace) -> None: UIFormatter.print_error(f"Invalid app name '{name}': {error_msg}") sys.exit(1) - # Create apps sequentially any_failure = False for name in deduped_names: app_manager = AppManager(name) diff --git a/djinit/scripts/console_ui.py b/djinit/scripts/console_ui.py index c8c2213..3479e87 100644 --- a/djinit/scripts/console_ui.py +++ b/djinit/scripts/console_ui.py @@ -45,10 +45,6 @@ class Icons: class UIFormatter: """Utility class for consistent UI formatting""" - # ============================================================================ - # Basic Output Methods - # ============================================================================ - @staticmethod def print_success(message: str, icon: str = Icons.SUCCESS): """Print success message""" @@ -74,10 +70,6 @@ class UIFormatter: else: console.print(f"[{UIColors.INFO}]{message}[/{UIColors.INFO}]") - # ============================================================================ - # Progress & Headers - # ============================================================================ - @staticmethod def create_live_progress(description: str = "Setup Progress", total_steps: int = 100): """Create a live progress display""" @@ -104,10 +96,6 @@ class UIFormatter: """Print a visual separator""" console.print(f"[{style}]{char * width}[/{style}]") - # ============================================================================ - # Panels & Tables - # ============================================================================ - @staticmethod def print_panel(content: str, title: str = "", border_style: str = "blue"): """Print content in a styled panel""" @@ -130,10 +118,6 @@ class UIFormatter: console.print(table) - # ============================================================================ - # Welcome & Summary - # ============================================================================ - @staticmethod def create_welcome_panel(): """Create a minimal welcome panel""" @@ -171,10 +155,6 @@ class UIFormatter: console.print("[red]Setup encountered an error. Check messages above.[/red]\n") console.print(f"[bold red]{'═' * 70}[/bold red]\n") - # ============================================================================ - # User Input - # ============================================================================ - @staticmethod @contextmanager def status(message: str, spinner: str = "dots"): diff --git a/djinit/scripts/django_helper.py b/djinit/scripts/django_helper.py index c8c21ab..e536690 100644 --- a/djinit/scripts/django_helper.py +++ b/djinit/scripts/django_helper.py @@ -16,7 +16,6 @@ from djinit.utils import ( class DjangoHelper: - # Django version to use in generated files (matches Django 5.2) DJANGO_VERSION = "5.2" @staticmethod @@ -24,22 +23,16 @@ class DjangoHelper: try: os.makedirs(directory, exist_ok=True) - # Create manage.py manage_py_path = os.path.join(directory, "manage.py") create_file_from_template(manage_py_path, "project/manage_py.j2", {}, "Created manage.py") os.chmod(manage_py_path, 0o755) - # For unified structure, the project config is "core", not project_name - # The unified structure is created separately by create_unified_structure if unified: - # Just create manage.py for unified, structure is created by create_unified_structure return True - # Create project config directory project_config_dir = os.path.join(directory, project_name) 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") create_directory_with_init(settings_dir, f"Created {project_name}/settings/__init__.py") @@ -51,7 +44,6 @@ class DjangoHelper: ] 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} project_files = [ ("urls.py", "config/urls/base.j2", urls_context), @@ -74,7 +66,6 @@ class DjangoHelper: context = {"app_name": app_name, "django_version": DjangoHelper.DJANGO_VERSION} - # Create app files create_init_file(app_dir, f"Created {app_name}/__init__.py") app_files = [ @@ -90,7 +81,6 @@ class DjangoHelper: 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") create_directory_with_init(migrations_dir, f"Created {app_name}/migrations/__init__.py") diff --git a/djinit/scripts/files.py b/djinit/scripts/files.py index f9b37fe..3aad629 100644 --- a/djinit/scripts/files.py +++ b/djinit/scripts/files.py @@ -23,8 +23,6 @@ class FileManager: self.project_name = project_name self.app_names = app_names self.metadata = metadata - # Use module name (e.g., 'config') for Django package paths if provided - # Fall back to project_name if key present but None self.module_name = metadata.get("project_module_name") or project_name self.project_configs = os.path.join(project_root, self.module_name) self.settings_folder = os.path.join(self.project_configs, "settings") @@ -105,7 +103,6 @@ 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") package_name = get_package_name(package_name) context = {"package_name": package_name, "project_name": self.project_name} @@ -142,16 +139,13 @@ class FileManager: ) def create_predefined_structure(self) -> bool: - # Ensure apps package apps_dir = os.path.join(self.project_root, "apps") create_directory_with_init(apps_dir, "Created apps/__init__.py") - # users app users_dir = os.path.join(apps_dir, "users") 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 = { "models": [("user.py", "presets/predefined/apps/users/models.j2")], "serializers": [("user_serializer.py", "presets/predefined/apps/users/serializers.j2")], @@ -161,7 +155,6 @@ class FileManager: } self._create_files_from_specs(users_dir, users_subfolders, {"app_module": "apps.users"}) - # users urls.py at app root users_urls_path = os.path.join(users_dir, "urls.py") create_file_from_template( users_urls_path, @@ -170,11 +163,9 @@ class FileManager: "Created apps/users/urls.py", ) - # core app core_dir = os.path.join(apps_dir, "core") create_directory_with_init(core_dir, "Created apps/core/__init__.py") - # core subfolders and files core_subfolders = { "utils": [("responses.py", "presets/predefined/core/utils/responses.j2")], "mixins": [("timestamped_model.py", "presets/predefined/core/mixins/timestamped_model.j2")], @@ -182,25 +173,21 @@ class FileManager: } self._create_files_from_specs(core_dir, core_subfolders, {}) - # core/exceptions.py exceptions_path = os.path.join(core_dir, "exceptions.py") create_file_from_template( exceptions_path, "presets/predefined/core/exceptions.j2", {}, "Created apps/core/exceptions.py" ) - # api package api_dir = os.path.join(self.project_root, "api") create_directory_with_init(api_dir, "Created api/__init__.py") api_urls_path = os.path.join(api_dir, "urls.py") create_file_from_template(api_urls_path, "presets/predefined/api/urls.j2", {}, "Created api/urls.py") - # api/v1 api_v1_dir = os.path.join(api_dir, "v1") create_directory_with_init(api_v1_dir, "Created api/v1/__init__.py") api_v1_urls_path = os.path.join(api_v1_dir, "urls.py") create_file_from_template(api_v1_urls_path, "presets/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") create_file_from_template( project_urls_path, @@ -213,21 +200,16 @@ class FileManager: return True def create_unified_structure(self) -> bool: - # Create core directory structure core_dir = os.path.join(self.project_root, "core") create_directory_with_init(core_dir, "Created core/__init__.py") - # Create core/settings directory settings_dir = os.path.join(core_dir, "settings") 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 secret_key = generate_secret_key() - # Create settings files using project templates with unified context - # For unified structure, project_module_name is "core" base_context = { "project_name": "core", # Use "core" as the module name "app_names": ["apps.core", "apps.api"] + [f"apps.{app}" for app in self.app_names], @@ -235,7 +217,6 @@ class FileManager: } dev_context = {"secret_key": secret_key} - # Settings files for filename, context in [ ("base.py", base_context), ("development.py", dev_context), @@ -247,7 +228,6 @@ class FileManager: filepath, template_path, context, f"Created core/settings/{filename}", should_format=True ) - # Core project files core_files = [ ("urls.py", "config/urls/with_api.j2", {"api_module": "apps.api"}), ("wsgi.py", "config/wsgi.j2", {}), @@ -257,18 +237,14 @@ class FileManager: filepath = os.path.join(core_dir, filename) 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") create_directory_with_init(apps_dir, "Created apps/__init__.py") - # Create apps/core apps_core_dir = os.path.join(apps_dir, "core") create_directory_with_init(apps_core_dir, "Created apps/core/__init__.py") - # Create apps/core/apps.py 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") model_files = [ ("__init__.py", "components/models.j2"), @@ -280,7 +256,6 @@ class FileManager: 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") utils_files = [ ("__init__.py", "presets/unified/apps/core/utils/__init__.py.j2"), @@ -291,15 +266,12 @@ class FileManager: utils_dir, [(f, t, {}) for f, t in utils_files], "apps/core/utils/", should_format=True ) - # Create apps/core/permissions and middleware self._create_subdirectories_with_init(apps_core_dir, ["permissions", "middleware"], "apps/core/") - # Create apps/api apps_api_dir = os.path.join(apps_dir, "api") 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 self._create_subdirectories_with_init( apps_api_dir, ["models", "serializers", "views", "services", "tests", "urls", "admin"], "apps/api/" ) @@ -307,20 +279,16 @@ class FileManager: return True def create_single_structure(self) -> bool: - # Create project directory structure (project/) project_dir = os.path.join(self.project_root, self.module_name) create_directory_with_init(project_dir, f"Created {self.module_name}/__init__.py") - # Create project/settings directory settings_dir = os.path.join(project_dir, "settings") create_directory_with_init(settings_dir, f"Created {self.module_name}/settings/__init__.py") - # Generate secret key for development settings from djinit.scripts.secretkey_generator import generate_secret_key secret_key = generate_secret_key() - # Create settings files base_context = { "project_name": self.module_name, "app_names": [self.module_name], # Project acts as the app @@ -339,7 +307,6 @@ class FileManager: filepath, template_path, context, f"Created {self.module_name}/settings/{filename}", should_format=True ) - # Core project files project_files = [ ( "urls.py", @@ -355,15 +322,9 @@ class FileManager: filepath, template, context, f"Created {self.module_name}/{filename}", should_format=True ) - # Create app component directories - # Note: routes, serializers, views are NOT created here - # They should be organized under api/ by model name (e.g., api/user/views.py) components = ["admin", "api", "models", "tests"] self._create_subdirectories_with_init(project_dir, components, f"{self.module_name}/") - # Create basic files for components to ensure valid python packages/modules - - # admin create_file_from_template( os.path.join(project_dir, "admin", "__init__.py"), "components/admin.j2", @@ -371,7 +332,6 @@ class FileManager: f"Created {self.module_name}/admin/__init__.py", ) - # models create_file_from_template( os.path.join(project_dir, "models", "__init__.py"), "components/models.j2", @@ -379,7 +339,6 @@ class FileManager: f"Created {self.module_name}/models/__init__.py", ) - # Create README files for api and models directories create_file_from_template( os.path.join(project_dir, "api", "README.md"), "components/api_readme.j2", diff --git a/djinit/scripts/input_handler.py b/djinit/scripts/input_handler.py index 167d7bf..4a5d721 100644 --- a/djinit/scripts/input_handler.py +++ b/djinit/scripts/input_handler.py @@ -35,7 +35,6 @@ class ProjectMetadata: nested_apps: bool = False nested_dir: str | None = None use_database_url: bool = False - # predefined structure support predefined_structure: bool = False predefined_structure: bool = False unified_structure: bool = False @@ -170,7 +169,6 @@ class InputCollector: f"[{UIColors.HIGHLIGHT}]Enter project directory name:[/{UIColors.HIGHLIGHT}] " ).strip() - # Handle empty input or '.' - use current directory if not user_input or user_input == ".": UIFormatter.print_info(f"Creating project in current directory: {os.getcwd()}") return "." @@ -370,14 +368,12 @@ class CharReader: @staticmethod def get_yes_no(prompt: str, default: str = None) -> str: - # Auto-detect default from prompt format (capital letter indicates default) if default is None: if "(Y/n)" in prompt or "(Y/N)" in prompt: default = "y" elif "(y/N)" in prompt: default = "n" else: - # Default to 'y' for yes/no prompts default = "y" return CharReader._get_validated_char_input( @@ -416,7 +412,6 @@ class CharReader: for encoding in encodings: try: ch = msvcrt.getch() - # Detect Ctrl+C (ETX character \x03) if isinstance(ch, bytes): if ch == b"\x03": raise KeyboardInterrupt @@ -452,7 +447,6 @@ 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() @@ -465,7 +459,6 @@ def get_user_input() -> Tuple[str, str, str, list, dict]: collector = InputCollector() console.print() - # 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}]") @@ -491,7 +484,6 @@ def get_user_input() -> Tuple[str, str, str, list, dict]: UIFormatter.print_error("Maximum attempts reached for project directory name. Exiting.") sys.exit(1) - # Ask for module name after project directory for single layout single_module_name = None if use_single: console.print() @@ -517,7 +509,6 @@ def get_user_input() -> Tuple[str, str, str, list, dict]: return project_dir, project_name, primary_app, app_names, metadata_dict - # 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}]") @@ -536,7 +527,6 @@ def get_user_input() -> Tuple[str, str, str, list, dict]: ) console.print() - # Section 2: Django Apps console.print() UIFormatter.print_separator() console.print(f"\n[{UIColors.INFO}]Step 2: Django Apps[/{UIColors.INFO}]\n") @@ -544,14 +534,10 @@ def get_user_input() -> Tuple[str, str, str, list, dict]: 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 = get_package_name(project_dir) metadata = ProjectMetadata( package_name=package_name, @@ -564,7 +550,6 @@ def get_user_input() -> Tuple[str, str, str, list, dict]: console.print() - # 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.") diff --git a/djinit/scripts/name_validator.py b/djinit/scripts/name_validator.py index b76d3a1..cf42b91 100644 --- a/djinit/scripts/name_validator.py +++ b/djinit/scripts/name_validator.py @@ -8,10 +8,8 @@ import re import sys from typing import Any, Callable, Tuple -# Names that conflict with Django's structure DJANGO_RESERVED = {"django", "test", "site-packages", "admin"} -# Common Python builtin modules PYTHON_BUILTINS = set(sys.builtin_module_names) NAME_PATTERN = re.compile(r"^[a-zA-Z][a-zA-Z0-9_]*$") diff --git a/djinit/scripts/project.py b/djinit/scripts/project.py index d3338c8..3c66e28 100644 --- a/djinit/scripts/project.py +++ b/djinit/scripts/project.py @@ -24,8 +24,6 @@ class ProjectManager: self.app_names = app_names self.metadata = metadata 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 def create_project(self) -> bool: @@ -59,7 +57,6 @@ class ProjectManager: return False 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(): return False @@ -113,7 +110,6 @@ class ProjectManager: 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/utils.py b/djinit/utils.py index c0b8506..8379a2e 100644 --- a/djinit/utils.py +++ b/djinit/utils.py @@ -17,7 +17,6 @@ def format_file(filename: str) -> None: subprocess.run([sys.executable, "-m", "ruff", "format", filename], check=False, capture_output=True) -# create a file with content, optionally format it def create_file_with_content(filename: str, content: str, success_message: str, should_format: bool = False) -> bool: with open(filename, "w") as file: file.write(content) @@ -25,11 +24,9 @@ def create_file_with_content(filename: str, content: str, success_message: str, if should_format: format_file(filename) - # UIFormatter.print_success(success_message) return True -# create a file from a template def create_file_from_template( file_path: str, template_path: str, @@ -41,7 +38,6 @@ def create_file_from_template( create_file_with_content(file_path, content, success_message, should_format=should_format) -# create __init__.py def create_init_file(directory: str, success_message: str) -> None: init_path = os.path.join(directory, "__init__.py") create_file_from_template(init_path, "project/init.j2", {}, success_message, should_format=False) @@ -79,7 +75,6 @@ def get_package_name(project_dir: str) -> str: return "backend" if project_dir == "." or not project_dir else project_dir -# change the current working dir tempporary @contextmanager def change_cwd(path: str): original_cwd = os.getcwd() @@ -118,7 +113,6 @@ def _iterate_user_apps_lines(content: str): 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): @@ -128,17 +122,14 @@ def extract_existing_apps(content: str) -> set: return existing_apps -# format app names as list entries with proper indentation def format_app_entries(apps: list) -> list: return [f' "{app}",' for app in apps] -# check if line starts a new APP section def is_app_section_start(line: str) -> bool: return "=" in line and any(keyword in line for keyword in ("BUILT_IN_APPS", "THIRD_PARTY_APPS", "INSTALLED_APPS")) -# check if closing bracket belongs to USER_DEFINED_APPS by looking ahead def is_user_apps_closing_bracket(lines: list, current_idx: int) -> bool: for next_idx in range(current_idx + 1, min(current_idx + 3, len(lines))): next_line = lines[next_idx].strip() @@ -147,7 +138,6 @@ def is_user_apps_closing_bracket(lines: list, current_idx: int) -> bool: return True # Default to True if we're inside USER_DEFINED_APPS -# split line with closing bracnket and insert apps def split_bracket_line(line: str, apps_to_add: list) -> list: bracket_pos = line.rfind("]") before_bracket = line[:bracket_pos].rstrip() if bracket_pos > 0 else line.rstrip() @@ -159,7 +149,6 @@ def split_bracket_line(line: str, apps_to_add: list) -> list: return result -# insert apps into USER_DEFINED_APPS section before the closing bracket. def insert_apps_into_user_defined_apps(content: str, apps_to_add: list) -> str: lines = content.split("\n") new_lines = [] @@ -167,11 +156,9 @@ def insert_apps_into_user_defined_apps(content: str, apps_to_add: list) -> str: apps_added = False for i, line in enumerate(lines): - # Entering USER_DEFINED_APPS section if _is_user_apps_section_start(line) and not apps_added: in_user_apps = True - # Handle closing bracket on same line if "]" in line: new_lines.extend(split_bracket_line(line, apps_to_add)) apps_added = True @@ -179,7 +166,6 @@ def insert_apps_into_user_defined_apps(content: str, apps_to_add: list) -> str: else: new_lines.append(line) - # Closing bracket on separate line elif in_user_apps and line.strip() == "]" and not apps_added: if is_user_apps_closing_bracket(lines, i): new_lines.extend(format_app_entries(apps_to_add)) @@ -189,7 +175,6 @@ def insert_apps_into_user_defined_apps(content: str, apps_to_add: list) -> str: else: new_lines.append(line) - # Safety: hit another APP section without closing bracket elif in_user_apps and is_app_section_start(line) and not apps_added: new_lines.extend(format_app_entries(apps_to_add)) new_lines.append("]") @@ -207,19 +192,16 @@ def insert_apps_into_user_defined_apps(content: str, apps_to_add: list) -> str: return "\n".join(new_lines) -# calculate app module paths based on nested structure 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 [calculate_app_module_path(app_name, nested, nested_dir_name) for app_name in app_names] -# calculate app module path for a single app def calculate_app_module_path(app_name: str, nested: bool, nested_dir: str | None) -> str: return f"{nested_dir}.{app_name}" if nested and nested_dir else app_name -# check if a dir is a Django project by looking for manage.py def is_django_project(directory: str = None) -> bool: if directory is None: directory = os.getcwd() @@ -227,7 +209,6 @@ def is_django_project(directory: str = None) -> bool: return os.path.exists(manage_py_path) -# search the project dir that contains settings/base.py def find_project_dir(search_dir: str = None) -> tuple[str | None, str | None]: if search_dir is None: search_dir = os.getcwd() @@ -241,7 +222,6 @@ def find_project_dir(search_dir: str = None) -> tuple[str | None, str | None]: return None, None -# search the project_name/settings dir path def find_settings_path(search_dir: str = None) -> str | None: project_dir, settings_base_path = find_project_dir(search_dir) if settings_base_path: @@ -249,13 +229,11 @@ def find_settings_path(search_dir: str = None) -> str | None: return None -# get the path to project_name/settings/base.py def get_base_settings_path(project_root: str, project_name: str) -> str: settings_dir = os.path.join(project_root, project_name, "settings") return os.path.join(settings_dir, "base.py") -# read the content of project_name/settings/base.py def read_base_settings(project_root: str, project_name: str) -> str | None: base_settings_path = get_base_settings_path(project_root, project_name) if not os.path.exists(base_settings_path): @@ -265,7 +243,6 @@ def read_base_settings(project_root: str, project_name: str) -> str | None: return f.read() -# check for nested app structure(e.g: "apps.users" means nested) from project_name/settings/base.py def detect_nested_structure_from_settings( base_settings_path: str, search_dir: str = None ) -> tuple[bool, str | None, str]: diff --git a/pyproject.toml b/pyproject.toml index fe89128..8f66bc9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,7 @@ dependencies = [ "rich>=14.2.0", "ruff>=0.14.2", "jinja2>=3.1.0", + "pre-commit>=4.5.0", ] [project.urls] diff --git a/uv.lock b/uv.lock index b613f0c..52fddce 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,15 @@ version = 1 revision = 3 requires-python = ">=3.13" +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + [[package]] name = "click" version = "8.3.0" @@ -23,6 +32,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + [[package]] name = "djinitx" version = "0.2.1" @@ -30,6 +48,7 @@ source = { virtual = "." } dependencies = [ { name = "click" }, { name = "jinja2" }, + { name = "pre-commit" }, { name = "rich" }, { name = "ruff" }, ] @@ -38,10 +57,29 @@ dependencies = [ requires-dist = [ { name = "click", specifier = ">=8.3.0" }, { name = "jinja2", specifier = ">=3.1.0" }, + { name = "pre-commit", specifier = ">=4.5.0" }, { name = "rich", specifier = ">=14.2.0" }, { name = "ruff", specifier = ">=0.14.2" }, ] +[[package]] +name = "filelock" +version = "3.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922, upload-time = "2025-10-08T18:03:50.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" }, +] + +[[package]] +name = "identify" +version = "2.6.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -127,6 +165,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/9b/6a4ffb4ed980519da959e1cf3122fc6cb41211daa58dbae1c73c0e519a37/pre_commit-4.5.0.tar.gz", hash = "sha256:dc5a065e932b19fc1d4c653c6939068fe54325af8e741e74e88db4d28a4dd66b", size = 198428, upload-time = "2025-11-22T21:02:42.304Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/c4/b2d28e9d2edf4f1713eb3c29307f1a63f3d67cf09bdda29715a36a68921a/pre_commit-4.5.0-py2.py3-none-any.whl", hash = "sha256:25e2ce09595174d9c97860a95609f9f852c0614ba602de3561e267547f2335e1", size = 226429, upload-time = "2025-11-22T21:02:40.836Z" }, +] + [[package]] name = "pygments" version = "2.19.2" @@ -136,6 +208,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + [[package]] name = "rich" version = "14.2.0" @@ -174,3 +282,17 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/71/30/3758bcf9e0b6a4193a6f51abf84254aba00887dfa8c20aba18aa366c5f57/ruff-0.14.2-py3-none-win_amd64.whl", hash = "sha256:0df3424aa5c3c08b34ed8ce099df1021e3adaca6e90229273496b839e5a7e1af", size = 13565279, upload-time = "2025-10-23T19:36:56.578Z" }, { url = "https://files.pythonhosted.org/packages/2e/5d/aa883766f8ef9ffbe6aa24f7192fb71632f31a30e77eb39aa2b0dc4290ac/ruff-0.14.2-py3-none-win_arm64.whl", hash = "sha256:ea9d635e83ba21569fbacda7e78afbfeb94911c9434aff06192d9bc23fd5495a", size = 12554956, upload-time = "2025-10-23T19:36:58.714Z" }, ] + +[[package]] +name = "virtualenv" +version = "20.35.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size = 6028799, upload-time = "2025-10-29T06:57:40.511Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" }, +]