feat: add pre-commit support to project generation
This commit is contained in:
13 files changed
+126
-172
No files matched your search
+1
-20
@@ -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
|
||||
|
||||
+2
-7
@@ -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()
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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_]*$")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -44,6 +44,7 @@ dependencies = [
|
||||
"rich>=14.2.0",
|
||||
"ruff>=0.14.2",
|
||||
"jinja2>=3.1.0",
|
||||
"pre-commit>=4.5.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
|
||||
@@ -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" },
|
||||
]
|
||||
Reference in new issue
Block a user