Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7c5a99e41 | ||
|
|
68f8cb81b5 | ||
|
|
962fa3c504 | ||
|
|
f875893399 | ||
|
|
cd1054b071 | ||
|
|
e60f5a4c1b | ||
|
|
4d45581419 | ||
|
|
bde87b91a0 | ||
|
|
f008b08e8b | ||
|
|
8d67e7831c | ||
|
|
717fe0aec5 | ||
|
|
00fa9ae5f5 | ||
|
|
56d54c5f37 |
No files matched your search
@@ -11,9 +11,9 @@ jobs:
|
||||
permissions:
|
||||
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.12'
|
||||
cache: 'pip'
|
||||
|
||||
@@ -10,5 +10,6 @@
|
||||
## Enhancements
|
||||
|
||||
- [ ] Add more project structure templates
|
||||
- [ ] Add interactive configuration wizard
|
||||
- [x] Add interactive configuration wizard
|
||||
- [ ] Add testing framework setup (pytest, coverage)
|
||||
- [x] Improve structure detection
|
||||
+8
-8
@@ -9,7 +9,6 @@ from typing import Callable, List, Tuple
|
||||
from djinit.services.files import FileManager
|
||||
from djinit.services.project import ProjectManager
|
||||
from djinit.ui.console import UIFormatter
|
||||
from djinit.utils.security import generate_secret_command
|
||||
|
||||
|
||||
class Cli:
|
||||
@@ -27,7 +26,8 @@ class Cli:
|
||||
self.project_manager = ProjectManager(project_dir, project_name, app_names, metadata)
|
||||
self.file_manager = FileManager(self.project_root, project_name, app_names, metadata)
|
||||
|
||||
def run_setup(self) -> bool:
|
||||
def _normalize_metadata(self) -> None:
|
||||
"""Normalize metadata and app names based on structure type."""
|
||||
if self.metadata.get("predefined_structure"):
|
||||
self.metadata.setdefault("project_module_name", "config")
|
||||
self.metadata.setdefault("nested_apps", True)
|
||||
@@ -36,7 +36,7 @@ class Cli:
|
||||
self.app_names = ["users", "core"]
|
||||
self.project_manager.app_names = self.app_names
|
||||
|
||||
if self.metadata.get("unified_structure"):
|
||||
elif self.metadata.get("unified_structure"):
|
||||
self.metadata.setdefault("project_module_name", "core")
|
||||
self.metadata.setdefault("nested_apps", True)
|
||||
self.metadata.setdefault("nested_dir", "apps")
|
||||
@@ -44,12 +44,15 @@ class Cli:
|
||||
self.app_names = []
|
||||
self.project_manager.app_names = self.app_names
|
||||
|
||||
if self.metadata.get("single_structure"):
|
||||
elif self.metadata.get("single_structure"):
|
||||
self.metadata.setdefault("nested_apps", False)
|
||||
if not self.app_names:
|
||||
self.app_names = []
|
||||
self.project_manager.app_names = self.app_names
|
||||
|
||||
def run_setup(self) -> bool:
|
||||
self._normalize_metadata()
|
||||
|
||||
steps: List[Tuple[str, Callable[[], None]]] = []
|
||||
|
||||
steps.append(("Creating Django project", self.project_manager.create_project))
|
||||
@@ -100,6 +103,7 @@ class Cli:
|
||||
self.file_manager.create_requirements,
|
||||
self.file_manager.create_readme,
|
||||
self.file_manager.create_env_file,
|
||||
self.file_manager.create_djinit_config,
|
||||
lambda: self.file_manager.create_pyproject_toml(self.metadata),
|
||||
]
|
||||
|
||||
@@ -114,7 +118,3 @@ class Cli:
|
||||
|
||||
if self.metadata.get("use_gitlab_ci", True):
|
||||
self.file_manager.create_gitlab_ci()
|
||||
|
||||
def generate_secret_keys(self) -> bool:
|
||||
generate_secret_command()
|
||||
return True
|
||||
@@ -11,7 +11,7 @@ from rich.text import Text
|
||||
|
||||
from djinit.services.app import AppManager
|
||||
from djinit.ui.console import UIColors, UIFormatter, console
|
||||
from djinit.utils.security import display_secret_keys, generate_multiple_keys
|
||||
from djinit.utils.secretkey import display_secret_keys, generate_multiple_keys
|
||||
from djinit.utils.validators import validate_app_name
|
||||
|
||||
|
||||
@@ -67,27 +67,17 @@ def handle_app_command(args: argparse.Namespace) -> None:
|
||||
UIFormatter.print_header("Django App Creation")
|
||||
UIFormatter.print_info("")
|
||||
|
||||
tokens = args.app_name if isinstance(args.app_name, list) else [args.app_name]
|
||||
pieces = []
|
||||
for token in tokens:
|
||||
if token is None:
|
||||
continue
|
||||
for part in str(token).split(","):
|
||||
part = part.strip()
|
||||
if part:
|
||||
pieces.append(part)
|
||||
app_names = pieces
|
||||
raw_tokens = args.app_name if isinstance(args.app_name, list) else [args.app_name]
|
||||
|
||||
# Flatten and clean tokens
|
||||
app_names = [part.strip() for token in raw_tokens if token for part in str(token).split(",") if part.strip()]
|
||||
|
||||
if not app_names:
|
||||
UIFormatter.print_error("App name cannot be empty")
|
||||
sys.exit(1)
|
||||
|
||||
seen = set()
|
||||
deduped_names = []
|
||||
for name in app_names:
|
||||
if name not in seen:
|
||||
deduped_names.append(name)
|
||||
seen.add(name)
|
||||
# Deduplicate while preserving order
|
||||
deduped_names = list(dict.fromkeys(app_names))
|
||||
|
||||
for name in deduped_names:
|
||||
is_valid, error_msg = validate_app_name(name)
|
||||
|
||||
+37
-13
@@ -16,6 +16,7 @@ from djinit.utils.common import (
|
||||
extract_existing_apps,
|
||||
find_project_dir,
|
||||
find_settings_path,
|
||||
get_djinit_config,
|
||||
insert_apps_into_user_defined_apps,
|
||||
is_django_project,
|
||||
)
|
||||
@@ -27,6 +28,7 @@ class AppManager:
|
||||
self.app_name = app_name
|
||||
self.current_dir = os.getcwd()
|
||||
self.manage_py_path = os.path.join(self.current_dir, "manage.py")
|
||||
self.config = get_djinit_config(self.current_dir)
|
||||
self._project_structure_cache = None
|
||||
|
||||
def create_app(self) -> bool:
|
||||
@@ -126,36 +128,58 @@ 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:
|
||||
project_dir, settings_base_path = find_project_dir(self.current_dir)
|
||||
if self.config:
|
||||
apps_config = self.config.get("apps", {})
|
||||
nested = apps_config.get("nested", False)
|
||||
nested_dir = apps_config.get("nested_dir")
|
||||
|
||||
if project_dir is None:
|
||||
self._project_structure_cache = (False, None, self.current_dir)
|
||||
# Determine base dir for apps
|
||||
if nested and nested_dir:
|
||||
apps_base_dir = os.path.join(self.current_dir, nested_dir)
|
||||
else:
|
||||
apps_base_dir = self.current_dir
|
||||
|
||||
self._project_structure_cache = (nested, nested_dir, apps_base_dir)
|
||||
else:
|
||||
self._project_structure_cache = detect_nested_structure_from_settings(
|
||||
settings_base_path, self.current_dir
|
||||
)
|
||||
# Fallback to old detection method
|
||||
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:
|
||||
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:
|
||||
if self.config:
|
||||
return self.config.get("structure", {}).get("predefined", False)
|
||||
|
||||
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."""
|
||||
apps_api_dir = os.path.join(self.current_dir, "apps", "api")
|
||||
if os.path.isdir(apps_api_dir):
|
||||
if self.config:
|
||||
structure = self.config.get("structure", {})
|
||||
return structure.get("unified", False) or structure.get("single", False)
|
||||
|
||||
# Check for Unified structure (apps/api exists)
|
||||
if os.path.isdir(os.path.join(self.current_dir, "apps", "api")):
|
||||
return True
|
||||
|
||||
apps_dir = os.path.join(self.current_dir, "apps")
|
||||
if os.path.isdir(apps_dir):
|
||||
# Check for Predefined structure (apps/ exists but not apps/api)
|
||||
if os.path.isdir(os.path.join(self.current_dir, "apps")):
|
||||
return False
|
||||
|
||||
# Check for Single Folder structure (models/ and api/ exist in project dir)
|
||||
project_dir, _ = find_project_dir(self.current_dir)
|
||||
if project_dir:
|
||||
models_dir = os.path.join(project_dir, "models")
|
||||
api_dir = os.path.join(project_dir, "api")
|
||||
if os.path.isdir(models_dir) and os.path.isdir(api_dir):
|
||||
has_models = os.path.isdir(os.path.join(project_dir, "models"))
|
||||
has_api = os.path.isdir(os.path.join(project_dir, "api"))
|
||||
if has_models and has_api:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
+98
-66
@@ -45,10 +45,12 @@ class FileManager:
|
||||
def _create_files_from_specs(self, base_dir: str, folder_specs: dict, base_context: dict = None) -> None:
|
||||
"""Helper to create multiple folders and files from a specification dict."""
|
||||
base_context = base_context or {}
|
||||
for folder, files in folder_specs.items():
|
||||
folder_path = os.path.join(base_dir, folder)
|
||||
|
||||
for folder_name, file_templates in folder_specs.items():
|
||||
folder_path = os.path.join(base_dir, folder_name)
|
||||
create_directory_with_init(folder_path, f"Created {folder_path}/__init__.py")
|
||||
for filename, template_path in files:
|
||||
|
||||
for filename, template_path in file_templates:
|
||||
file_path = os.path.join(folder_path, filename)
|
||||
create_file_from_template(file_path, template_path, base_context, f"Created {file_path}")
|
||||
|
||||
@@ -66,6 +68,52 @@ class FileManager:
|
||||
message = f"Created {prefix}{subdir}/__init__.py" if prefix else f"Created {subdir}/__init__.py"
|
||||
create_directory_with_init(subdir_path, message)
|
||||
|
||||
def _create_settings_package(self, settings_dir: str, base_context: dict, prefix: str) -> None:
|
||||
"""Helper to create settings package files (base, development, production)."""
|
||||
from djinit.utils.secretkey import generate_secret_key
|
||||
|
||||
secret_key = generate_secret_key()
|
||||
dev_context = {"secret_key": secret_key}
|
||||
|
||||
for filename, context in [
|
||||
("base.py", base_context),
|
||||
("development.py", dev_context),
|
||||
("production.py", base_context),
|
||||
]:
|
||||
filepath = os.path.join(settings_dir, filename)
|
||||
template_path = f"config/settings/{filename.replace('.py', '')}.j2"
|
||||
self._render_and_create_file(
|
||||
filepath,
|
||||
template_path,
|
||||
context,
|
||||
f"Created {prefix}/settings/{filename}",
|
||||
base_dir=os.path.dirname(
|
||||
settings_dir
|
||||
), # Not strictly used by _render_and_create_file but good for context
|
||||
should_format=True,
|
||||
)
|
||||
|
||||
def _create_lifecycle_files(
|
||||
self, target_dir: str, prefix: str, api_module: str = None, comment_out_api: bool = False
|
||||
) -> None:
|
||||
"""Helper to create urls.py, wsgi.py, and asgi.py."""
|
||||
|
||||
urls_context = {"api_module": api_module}
|
||||
if comment_out_api:
|
||||
urls_context["comment_out_api_url"] = True
|
||||
|
||||
files = [
|
||||
("urls.py", "config/urls/with_api.j2", urls_context),
|
||||
("wsgi.py", "config/wsgi.j2", {}),
|
||||
("asgi.py", "config/asgi.j2", {}),
|
||||
]
|
||||
|
||||
for filename, template, context in files:
|
||||
filepath = os.path.join(target_dir, filename)
|
||||
self._render_and_create_file(
|
||||
filepath, template, context, f"Created {prefix}/{filename}", should_format=True
|
||||
)
|
||||
|
||||
def create_gitignore(self) -> None:
|
||||
self._render_and_create_file(".gitignore", "project/gitignore.j2", {}, "Created .gitignore file")
|
||||
|
||||
@@ -208,43 +256,17 @@ class FileManager:
|
||||
settings_dir = os.path.join(core_dir, "settings")
|
||||
create_directory_with_init(settings_dir, "Created core/settings/__init__.py")
|
||||
|
||||
from djinit.utils.security import generate_secret_key
|
||||
|
||||
secret_key = generate_secret_key()
|
||||
|
||||
# In this new structure, 'apps' is the main app.
|
||||
# We assume the user wants 'apps' to be the app label.
|
||||
# Or perhaps 'apps' is just a container and the components inside are what matters?
|
||||
# Given the structure: apps/models, apps/views etc.
|
||||
# It seems 'apps' is acting as the app.
|
||||
|
||||
# Create settings files
|
||||
base_context = {
|
||||
"project_name": "core", # Project module is 'core'
|
||||
"app_names": ["apps"], # 'apps' is the main app
|
||||
"project_name": "core",
|
||||
"app_names": ["apps"],
|
||||
"use_database_url": self.metadata.get("use_database_url", True),
|
||||
"database_type": self.metadata.get("database_type", "postgresql"),
|
||||
}
|
||||
dev_context = {"secret_key": secret_key}
|
||||
self._create_settings_package(settings_dir, base_context, "core")
|
||||
|
||||
for filename, context in [
|
||||
("base.py", base_context),
|
||||
("development.py", dev_context),
|
||||
("production.py", base_context),
|
||||
]:
|
||||
filepath = os.path.join(settings_dir, filename)
|
||||
template_path = f"config/settings/{filename.replace('.py', '')}.j2"
|
||||
create_file_from_template(
|
||||
filepath, template_path, context, f"Created core/settings/{filename}", should_format=True
|
||||
)
|
||||
|
||||
core_files = [
|
||||
("urls.py", "config/urls/with_api.j2", {"api_module": "apps.api"}),
|
||||
("wsgi.py", "config/wsgi.j2", {}),
|
||||
("asgi.py", "config/asgi.j2", {}),
|
||||
]
|
||||
for filename, template, context in core_files:
|
||||
filepath = os.path.join(core_dir, filename)
|
||||
create_file_from_template(filepath, template, context, f"Created core/{filename}", should_format=True)
|
||||
# Create lifecycle files (urls, wsgi, asgi)
|
||||
self._create_lifecycle_files(core_dir, "core", api_module="apps.api")
|
||||
|
||||
# 2. Create 'apps' directory (The Main App)
|
||||
apps_dir = os.path.join(self.project_root, "apps")
|
||||
@@ -287,43 +309,19 @@ class FileManager:
|
||||
settings_dir = os.path.join(project_dir, "settings")
|
||||
create_directory_with_init(settings_dir, f"Created {self.module_name}/settings/__init__.py")
|
||||
|
||||
from djinit.utils.security 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
|
||||
"app_names": [self.module_name],
|
||||
"use_database_url": self.metadata.get("use_database_url", True),
|
||||
"database_type": self.metadata.get("database_type", "postgresql"),
|
||||
}
|
||||
dev_context = {"secret_key": secret_key}
|
||||
self._create_settings_package(settings_dir, base_context, self.module_name)
|
||||
|
||||
for filename, context in [
|
||||
("base.py", base_context),
|
||||
("development.py", dev_context),
|
||||
("production.py", base_context),
|
||||
]:
|
||||
filepath = os.path.join(settings_dir, filename)
|
||||
template_path = f"config/settings/{filename.replace('.py', '')}.j2"
|
||||
create_file_from_template(
|
||||
filepath, template_path, context, f"Created {self.module_name}/settings/{filename}", should_format=True
|
||||
)
|
||||
|
||||
project_files = [
|
||||
(
|
||||
"urls.py",
|
||||
"config/urls/with_api.j2",
|
||||
{"api_module": f"{self.module_name}.api", "comment_out_api_url": True},
|
||||
),
|
||||
("wsgi.py", "config/wsgi.j2", {}),
|
||||
("asgi.py", "config/asgi.j2", {}),
|
||||
]
|
||||
for filename, template, context in project_files:
|
||||
filepath = os.path.join(project_dir, filename)
|
||||
create_file_from_template(
|
||||
filepath, template, context, f"Created {self.module_name}/{filename}", should_format=True
|
||||
)
|
||||
# Create lifecycle files (urls, wsgi, asgi)
|
||||
self._create_lifecycle_files(
|
||||
project_dir, self.module_name, api_module=f"{self.module_name}.api", comment_out_api=True
|
||||
)
|
||||
|
||||
components = ["admin", "api", "models", "tests"]
|
||||
self._create_subdirectories_with_init(project_dir, components, f"{self.module_name}/")
|
||||
@@ -395,3 +393,37 @@ class FileManager:
|
||||
context,
|
||||
"Created GitLab CI configuration (.gitlab-ci.yml)",
|
||||
)
|
||||
|
||||
def create_djinit_config(self) -> None:
|
||||
"""Create .djinit configuration file."""
|
||||
import json
|
||||
|
||||
is_predefined = self.metadata.get("predefined_structure", False)
|
||||
is_unified = self.metadata.get("unified_structure", False)
|
||||
is_single = self.metadata.get("single_structure", False)
|
||||
is_standard = not (is_predefined or is_unified or is_single)
|
||||
|
||||
config = {
|
||||
"project_name": self.project_name,
|
||||
"structure": {
|
||||
"standard": is_standard,
|
||||
"predefined": is_predefined,
|
||||
"unified": is_unified,
|
||||
"single": is_single,
|
||||
},
|
||||
"apps": {
|
||||
"nested": self.metadata.get("nested_apps", False),
|
||||
"nested_dir": self.metadata.get("nested_dir"),
|
||||
},
|
||||
"settings": {
|
||||
"use_database_url": self.metadata.get("use_database_url", True),
|
||||
"database_type": self.metadata.get("database_type", "postgresql"),
|
||||
},
|
||||
"cicd": {
|
||||
"github": self.metadata.get("use_github_actions", False),
|
||||
"gitlab": self.metadata.get("use_gitlab_ci", False),
|
||||
},
|
||||
}
|
||||
|
||||
filepath = os.path.join(self.project_root, ".djinit")
|
||||
create_file_with_content(filepath, json.dumps(config, indent=4), "Created .djinit configuration file")
|
||||
+22
-19
@@ -89,23 +89,25 @@ class ProjectManager:
|
||||
UIFormatter.print_success(f"Added apps to USER_DEFINED_APPS: {added_apps_str}")
|
||||
|
||||
def validate_project_structure(self) -> None:
|
||||
join = lambda *args: os.path.join(self.project_root, *args)
|
||||
|
||||
required_files = [
|
||||
os.path.join(self.project_root, "manage.py"),
|
||||
os.path.join(self.project_root, self.module_name, "__init__.py"),
|
||||
os.path.join(self.project_root, self.module_name, "settings", "__init__.py"),
|
||||
os.path.join(self.project_root, self.module_name, "settings", "base.py"),
|
||||
os.path.join(self.project_root, self.module_name, "settings", "development.py"),
|
||||
os.path.join(self.project_root, self.module_name, "settings", "production.py"),
|
||||
os.path.join(self.project_root, self.module_name, "urls.py"),
|
||||
os.path.join(self.project_root, self.module_name, "wsgi.py"),
|
||||
os.path.join(self.project_root, self.module_name, "asgi.py"),
|
||||
join("manage.py"),
|
||||
join(self.module_name, "__init__.py"),
|
||||
join(self.module_name, "settings", "__init__.py"),
|
||||
join(self.module_name, "settings", "base.py"),
|
||||
join(self.module_name, "settings", "development.py"),
|
||||
join(self.module_name, "settings", "production.py"),
|
||||
join(self.module_name, "urls.py"),
|
||||
join(self.module_name, "wsgi.py"),
|
||||
join(self.module_name, "asgi.py"),
|
||||
]
|
||||
|
||||
apps_base_dir = self._get_apps_base_dir()
|
||||
|
||||
if self.metadata.get("unified_structure"):
|
||||
# Validate Unified Structure
|
||||
apps_dir = os.path.join(self.project_root, "apps")
|
||||
apps_dir = join("apps")
|
||||
unified_files = [
|
||||
os.path.join(apps_dir, "__init__.py"),
|
||||
os.path.join(apps_dir, "apps.py"),
|
||||
@@ -124,16 +126,17 @@ class ProjectManager:
|
||||
|
||||
elif not self.metadata.get("predefined_structure") and not self.metadata.get("single_structure"):
|
||||
for app_name in self.app_names:
|
||||
app_path = lambda f, app=app_name: os.path.join(apps_base_dir, app, f)
|
||||
app_files = [
|
||||
os.path.join(apps_base_dir, app_name, "__init__.py"),
|
||||
os.path.join(apps_base_dir, app_name, "apps.py"),
|
||||
os.path.join(apps_base_dir, app_name, "models.py"),
|
||||
os.path.join(apps_base_dir, app_name, "views.py"),
|
||||
os.path.join(apps_base_dir, app_name, "serializers.py"),
|
||||
os.path.join(apps_base_dir, app_name, "routes.py"),
|
||||
os.path.join(apps_base_dir, app_name, "tests.py"),
|
||||
os.path.join(apps_base_dir, app_name, "migrations"),
|
||||
os.path.join(apps_base_dir, app_name, "admin.py"),
|
||||
app_path("__init__.py"),
|
||||
app_path("apps.py"),
|
||||
app_path("models.py"),
|
||||
app_path("views.py"),
|
||||
app_path("serializers.py"),
|
||||
app_path("routes.py"),
|
||||
app_path("tests.py"),
|
||||
app_path("migrations"),
|
||||
app_path("admin.py"),
|
||||
]
|
||||
required_files.extend(app_files)
|
||||
|
||||
|
||||
+19
-10
@@ -6,6 +6,7 @@ Provides clean, minimal styling and user interface components.
|
||||
from contextlib import contextmanager
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import questionary
|
||||
from rich import box
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
@@ -164,20 +165,28 @@ class UIFormatter:
|
||||
|
||||
@staticmethod
|
||||
def confirm(prompt: str, default: bool = True) -> bool:
|
||||
"""Display a confirmation prompt"""
|
||||
default_str = "Y/n" if default else "y/N"
|
||||
response = console.input(f"[bold cyan]?[/bold cyan] {prompt} [{default_str}]: ").strip().lower()
|
||||
if not response:
|
||||
return default
|
||||
return response in ("y", "yes")
|
||||
"""Display a confirmation prompt as a selectable list"""
|
||||
choices = [
|
||||
questionary.Choice("Yes", value=True),
|
||||
questionary.Choice("No", value=False),
|
||||
]
|
||||
default_choice = choices[0] if default else choices[1]
|
||||
|
||||
result = questionary.select(
|
||||
prompt,
|
||||
choices=choices,
|
||||
default=default_choice,
|
||||
).ask()
|
||||
|
||||
if result is None:
|
||||
raise KeyboardInterrupt
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def prompt(message: str, default: Optional[str] = None) -> str:
|
||||
"""Display an input prompt"""
|
||||
if default:
|
||||
message = f"{message} [{default}]"
|
||||
response = console.input(f"[bold cyan]?[/bold cyan] {message}: ").strip()
|
||||
return response or default or ""
|
||||
return questionary.text(message, default=default or "").ask()
|
||||
|
||||
@staticmethod
|
||||
def handle_exception(e: Exception):
|
||||
|
||||
+110
-370
@@ -5,10 +5,9 @@ Handles collection and processing of user inputs during setup.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import termios
|
||||
import tty
|
||||
from enum import Enum
|
||||
from typing import Callable, Tuple
|
||||
from typing import Tuple, TypedDict
|
||||
|
||||
import questionary
|
||||
|
||||
from djinit.core.types import ProjectMetadata
|
||||
from djinit.ui.console import UIColors, UIFormatter, console
|
||||
@@ -16,77 +15,46 @@ from djinit.utils.common import get_package_name
|
||||
from djinit.utils.validators import validate_app_name, validate_project_name
|
||||
|
||||
|
||||
class CICDOption(Enum):
|
||||
GITHUB = ("g", "GitHub Actions only")
|
||||
GITLAB = ("l", "GitLab CI only")
|
||||
BOTH = ("b", "Both (GitHub Actions + GitLab CI)")
|
||||
NONE = ("n", "None (skip CI/CD)")
|
||||
|
||||
def __init__(self, key: str, description: str):
|
||||
self.key = key
|
||||
self.description = description
|
||||
class StructureOptions(TypedDict):
|
||||
project_dir: str
|
||||
predefined: bool
|
||||
unified: bool
|
||||
single: bool
|
||||
single_module_name: str | None
|
||||
database_type: str
|
||||
use_database_url: bool
|
||||
use_github: bool
|
||||
use_gitlab: bool
|
||||
|
||||
|
||||
class InputCollector:
|
||||
MAX_ATTEMPTS = 3
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def get_validated_input(
|
||||
self, prompt: str, validator: Callable[[str], Tuple[bool, str]], input_type: str, allow_empty: bool = False
|
||||
) -> str:
|
||||
attempt = 0
|
||||
def get_validated_input(self, prompt: str, validator: callable, input_type: str, allow_empty: bool = False) -> str:
|
||||
def q_validator(text):
|
||||
if not text and allow_empty:
|
||||
return True
|
||||
if not text:
|
||||
return f"{input_type.capitalize()} cannot be empty"
|
||||
is_valid, error_msg = validator(text)
|
||||
if is_valid:
|
||||
return True
|
||||
return error_msg
|
||||
|
||||
while attempt < self.MAX_ATTEMPTS:
|
||||
try:
|
||||
user_input = console.input(f"[{UIColors.HIGHLIGHT}]{prompt}:[/{UIColors.HIGHLIGHT}] ")
|
||||
|
||||
if not user_input.strip():
|
||||
if allow_empty:
|
||||
return ""
|
||||
UIFormatter.print_error(f"{input_type.capitalize()} cannot be empty")
|
||||
attempt += 1
|
||||
self._show_retry_message(attempt)
|
||||
continue
|
||||
|
||||
is_valid, error_msg = validator(user_input)
|
||||
|
||||
if is_valid:
|
||||
return user_input.strip()
|
||||
|
||||
UIFormatter.print_error(error_msg)
|
||||
attempt += 1
|
||||
self._show_retry_message(attempt)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
UIFormatter.print_info("\nSetup cancelled by user.")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
UIFormatter.print_error(f"Unexpected error: {str(e)}")
|
||||
attempt += 1
|
||||
self._show_retry_message(attempt)
|
||||
|
||||
UIFormatter.print_error(f"Maximum attempts reached for {input_type}. Exiting.")
|
||||
sys.exit(1)
|
||||
|
||||
def _show_retry_message(self, attempt: int, max_attempts: int = None) -> None:
|
||||
max_attempts = max_attempts or self.MAX_ATTEMPTS
|
||||
if attempt < max_attempts:
|
||||
console.print(f"[{UIColors.MUTED}]Please try again ({attempt}/{max_attempts}).[/{UIColors.MUTED}]")
|
||||
result = questionary.text(prompt, validate=q_validator).ask()
|
||||
if result is None:
|
||||
raise KeyboardInterrupt
|
||||
return result
|
||||
|
||||
def get_app_names(self) -> list[str]:
|
||||
console.print(
|
||||
f"[{UIColors.MUTED}]Enter app names separated by commas(e.g. users, products, orders)[/{UIColors.MUTED}]"
|
||||
)
|
||||
user_input = questionary.text(
|
||||
"Enter app names (comma-separated or single, e.g. users, products, orders):",
|
||||
validate=lambda text: True if text.strip() else "At least one app name is required",
|
||||
).ask()
|
||||
|
||||
user_input = console.input(
|
||||
f"[{UIColors.HIGHLIGHT}]Enter app names (comma-separated or single):[/{UIColors.HIGHLIGHT}] "
|
||||
)
|
||||
|
||||
if not user_input.strip():
|
||||
UIFormatter.print_error("At least one app name is required")
|
||||
return self.get_app_names()
|
||||
if user_input is None:
|
||||
raise KeyboardInterrupt
|
||||
|
||||
if "," in user_input:
|
||||
return self._parse_comma_separated_apps(user_input)
|
||||
@@ -120,76 +88,69 @@ class InputCollector:
|
||||
return app_list
|
||||
|
||||
def _get_project_directory(self) -> str | None:
|
||||
attempt = 0
|
||||
def validate_dir(text):
|
||||
if not text or text == ".":
|
||||
return True
|
||||
|
||||
while attempt < self.MAX_ATTEMPTS:
|
||||
try:
|
||||
user_input = console.input(
|
||||
f"[{UIColors.HIGHLIGHT}]Enter project directory name:[/{UIColors.HIGHLIGHT}] "
|
||||
).strip()
|
||||
is_valid, error_msg = validate_project_name(text)
|
||||
if not is_valid:
|
||||
return error_msg
|
||||
|
||||
if not user_input or user_input == ".":
|
||||
UIFormatter.print_info(f"Creating project in current directory: {os.getcwd()}")
|
||||
return "."
|
||||
if os.path.exists(text):
|
||||
return f"Directory '{text}' already exists."
|
||||
|
||||
is_valid, error_msg = validate_project_name(user_input)
|
||||
if not is_valid:
|
||||
UIFormatter.print_error(error_msg)
|
||||
attempt += 1
|
||||
self._show_retry_message(attempt)
|
||||
continue
|
||||
return True
|
||||
|
||||
if os.path.exists(user_input):
|
||||
UIFormatter.print_error(f"Directory '{user_input}' already exists.")
|
||||
UIFormatter.print_info(
|
||||
"Please choose a different project directory name (or press Enter for current directory)."
|
||||
)
|
||||
attempt += 1
|
||||
self._show_retry_message(attempt)
|
||||
continue
|
||||
project_dir = questionary.text("Enter project directory name:", validate=validate_dir).ask()
|
||||
|
||||
return user_input
|
||||
if project_dir is None:
|
||||
raise KeyboardInterrupt
|
||||
|
||||
except KeyboardInterrupt:
|
||||
UIFormatter.print_info("\nSetup cancelled by user.")
|
||||
sys.exit(0)
|
||||
if not project_dir or project_dir == ".":
|
||||
UIFormatter.print_info(f"Creating project in current directory: {os.getcwd()}")
|
||||
return "."
|
||||
|
||||
return None
|
||||
return project_dir
|
||||
|
||||
def _get_selection(self, message: str, choices: list[tuple[str, str]], default: str = None) -> str:
|
||||
q_choices = [questionary.Choice(title, value=value) for title, value in choices]
|
||||
default_choice = next((c for c in q_choices if c.value == default), q_choices[0])
|
||||
|
||||
result = questionary.select(
|
||||
message,
|
||||
choices=q_choices,
|
||||
default=default_choice,
|
||||
).ask()
|
||||
|
||||
if result is None:
|
||||
raise KeyboardInterrupt
|
||||
|
||||
return result
|
||||
|
||||
def get_cicd_choice(self) -> Tuple[bool, bool]:
|
||||
UIFormatter.print_separator()
|
||||
console.print(f"\n[{UIColors.INFO}]CI/CD Pipeline[/{UIColors.INFO}]\n")
|
||||
console.print()
|
||||
choices = [
|
||||
("None (skip CI/CD)", "none"),
|
||||
("GitHub Actions only", "github"),
|
||||
("GitLab CI only", "gitlab"),
|
||||
("Both (GitHub Actions + GitLab CI)", "both"),
|
||||
]
|
||||
|
||||
for option in CICDOption:
|
||||
console.print(f" [{UIColors.SUCCESS}]{option.key}[/{UIColors.SUCCESS}] {option.description}")
|
||||
console.print()
|
||||
|
||||
valid_keys = [option.key for option in CICDOption]
|
||||
|
||||
choice = CharReader.get_cicd_choice(
|
||||
f"[{UIColors.HIGHLIGHT}]Your choice: [/{UIColors.HIGHLIGHT}]",
|
||||
valid_keys=valid_keys,
|
||||
default=CICDOption.NONE.key,
|
||||
)
|
||||
choice = self._get_selection("Select CI/CD pipeline:", choices, default="none")
|
||||
|
||||
choice_map = {
|
||||
CICDOption.BOTH.key: (True, True),
|
||||
CICDOption.GITHUB.key: (True, False),
|
||||
CICDOption.GITLAB.key: (False, True),
|
||||
"both": (True, True),
|
||||
"github": (True, False),
|
||||
"gitlab": (False, True),
|
||||
"none": (False, False),
|
||||
}
|
||||
return choice_map.get(choice, (False, False))
|
||||
|
||||
def get_nested_apps_config(self) -> Tuple[bool, str | None]:
|
||||
UIFormatter.print_separator()
|
||||
console.print(
|
||||
f"[{UIColors.MUTED}]Do you want to place apps inside a package directory (e.g. 'src/')?[/{UIColors.MUTED}]"
|
||||
choice = UIFormatter.confirm(
|
||||
"Do you want to place apps inside a package directory (e.g. 'src/')?", default=False
|
||||
)
|
||||
console.print()
|
||||
|
||||
choice = CharReader.get_yes_no(f"[{UIColors.HIGHLIGHT}]Nested Django apps? (y/N):[/{UIColors.HIGHLIGHT}]")
|
||||
|
||||
if choice != "y":
|
||||
if not choice:
|
||||
return False, None
|
||||
|
||||
dir_name = self.get_validated_input(
|
||||
@@ -200,262 +161,59 @@ class InputCollector:
|
||||
return True, dir_name
|
||||
|
||||
def get_database_config_choice(self) -> bool:
|
||||
UIFormatter.print_separator()
|
||||
console.print()
|
||||
console.print(f" [{UIColors.SUCCESS}]Y[/{UIColors.SUCCESS}] Use DATABASE_URL (recommended for production)")
|
||||
console.print(f" [{UIColors.SUCCESS}]N[/{UIColors.SUCCESS}] Use individual database parameters")
|
||||
console.print()
|
||||
|
||||
return CharReader.get_yes_no(f"[{UIColors.HIGHLIGHT}]Use DATABASE_URL? (Y/n):[/{UIColors.HIGHLIGHT}]") == "y"
|
||||
return UIFormatter.confirm("Use DATABASE_URL? (recommended for production)", default=True)
|
||||
|
||||
def get_database_type_choice(self) -> str:
|
||||
console.print()
|
||||
console.print(f"[{UIColors.INFO}]Select Database Type[/{UIColors.INFO}]\n")
|
||||
console.print(f" [{UIColors.SUCCESS}]1[/{UIColors.SUCCESS}] PostgreSQL")
|
||||
console.print(f" [{UIColors.SUCCESS}]2[/{UIColors.SUCCESS}] MySQL")
|
||||
console.print()
|
||||
choices = [
|
||||
("PostgreSQL", "postgresql"),
|
||||
("MySQL", "mysql"),
|
||||
]
|
||||
|
||||
choice = CharReader.get_validated_char_input(
|
||||
f"[{UIColors.HIGHLIGHT}]Choose database (1/2):[/{UIColors.HIGHLIGHT}]",
|
||||
valid_keys=["1", "2"],
|
||||
default=None,
|
||||
error_message="Invalid input '{response}'. Please enter '1' or '2'.",
|
||||
)
|
||||
return self._get_selection("Choose database:", choices, default="postgresql")
|
||||
|
||||
return "mysql" if choice == "2" else "postgresql"
|
||||
|
||||
def _get_structure_metadata(
|
||||
self,
|
||||
project_dir: str,
|
||||
predefined: bool = False,
|
||||
unified: bool = False,
|
||||
single: bool = False,
|
||||
single_module_name: str = None,
|
||||
database_type: str = "postgresql",
|
||||
use_database_url: bool = True,
|
||||
use_github: bool = False,
|
||||
use_gitlab: bool = False,
|
||||
) -> Tuple[str, str, list[str], dict]:
|
||||
def _get_structure_metadata(self, options: StructureOptions) -> Tuple[str, str, list[str], dict]:
|
||||
"""Helper method to generate metadata dictionary."""
|
||||
project_dir = options["project_dir"]
|
||||
project_name = project_dir
|
||||
app_names: list[str] = []
|
||||
|
||||
# Default package_name to "backend" if project_dir is "." or empty
|
||||
package_name = get_package_name(project_dir)
|
||||
|
||||
project_module_name = "config" if predefined else "core" if unified else None
|
||||
project_module_name = "config" if options["predefined"] else "core" if options["unified"] else None
|
||||
|
||||
if single:
|
||||
project_module_name = single_module_name or "project"
|
||||
if options["single"]:
|
||||
project_module_name = options["single_module_name"] or "project"
|
||||
|
||||
metadata = ProjectMetadata(
|
||||
package_name=package_name,
|
||||
use_github_actions=use_github,
|
||||
use_gitlab_ci=use_gitlab,
|
||||
nested_apps=True,
|
||||
nested_dir="apps",
|
||||
use_database_url=use_database_url,
|
||||
database_type=database_type,
|
||||
predefined_structure=predefined,
|
||||
unified_structure=unified,
|
||||
single_structure=single,
|
||||
use_github_actions=options["use_github"],
|
||||
use_gitlab_ci=options["use_gitlab"],
|
||||
nested_apps=not options["single"],
|
||||
nested_dir="apps" if not options["single"] else None,
|
||||
use_database_url=options["use_database_url"],
|
||||
database_type=options["database_type"],
|
||||
predefined_structure=options["predefined"],
|
||||
unified_structure=options["unified"],
|
||||
single_structure=options["single"],
|
||||
project_module_name=project_module_name,
|
||||
)
|
||||
|
||||
return project_name, "", app_names, metadata.to_dict()
|
||||
|
||||
|
||||
class CharReader:
|
||||
MAX_ATTEMPTS = 3
|
||||
|
||||
@staticmethod
|
||||
def get_char() -> str:
|
||||
if os.name == "nt":
|
||||
return CharReader._get_char_windows()
|
||||
return CharReader._get_char_unix()
|
||||
|
||||
@staticmethod
|
||||
def _is_enter_key(response: str) -> bool:
|
||||
"""Check if response represents Enter key or control character."""
|
||||
return not response or response in ("\r", "\n") or ord(response) < 32
|
||||
|
||||
@staticmethod
|
||||
def _get_char_input(prompt: str) -> str:
|
||||
console.print(f"{prompt} ", end="")
|
||||
sys.stdout.flush()
|
||||
return CharReader().get_char()
|
||||
|
||||
@staticmethod
|
||||
def _handle_default(default: str) -> str:
|
||||
console.print(default.upper())
|
||||
return default.lower()
|
||||
|
||||
@staticmethod
|
||||
def get_validated_char_input(
|
||||
prompt: str,
|
||||
valid_keys: list[str],
|
||||
default: str | None,
|
||||
error_message: str,
|
||||
) -> str:
|
||||
attempt = 0
|
||||
valid_keys_lower = [k.lower() for k in valid_keys]
|
||||
|
||||
while attempt < CharReader.MAX_ATTEMPTS:
|
||||
try:
|
||||
response = CharReader._get_char_input(prompt)
|
||||
|
||||
# Handle Enter key (default)
|
||||
if CharReader._is_enter_key(response):
|
||||
if default is not None:
|
||||
return CharReader._handle_default(default)
|
||||
# If default is None, treat as invalid input (fall through)
|
||||
|
||||
response = response.lower()
|
||||
|
||||
# Validate response
|
||||
if response in valid_keys_lower:
|
||||
console.print(response.upper())
|
||||
return response
|
||||
|
||||
# Invalid input - show error
|
||||
console.print()
|
||||
if response.isprintable():
|
||||
UIFormatter.print_error(error_message.format(response=response))
|
||||
attempt += 1
|
||||
if attempt < CharReader.MAX_ATTEMPTS:
|
||||
console.print(
|
||||
f"[{UIColors.MUTED}]Please try again ({attempt}/{CharReader.MAX_ATTEMPTS}).[/{UIColors.MUTED}]"
|
||||
)
|
||||
console.print()
|
||||
else:
|
||||
# Control character entered (e.g. Enter with no default)
|
||||
if default is not None:
|
||||
return CharReader._handle_default(default)
|
||||
|
||||
UIFormatter.print_error("Input required.")
|
||||
attempt += 1
|
||||
if attempt < CharReader.MAX_ATTEMPTS:
|
||||
console.print(
|
||||
f"[{UIColors.MUTED}]Please try again ({attempt}/{CharReader.MAX_ATTEMPTS}).[/{UIColors.MUTED}]"
|
||||
)
|
||||
console.print()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
console.print()
|
||||
UIFormatter.print_info("\nOperation cancelled by user.")
|
||||
raise
|
||||
|
||||
# Max attempts reached
|
||||
console.print()
|
||||
if default is not None:
|
||||
UIFormatter.print_warning(f"Maximum attempts reached. Using default: '{default.upper()}'.")
|
||||
return default.lower()
|
||||
|
||||
UIFormatter.print_error("Maximum attempts reached. Exiting.")
|
||||
sys.exit(1)
|
||||
|
||||
@staticmethod
|
||||
def get_yes_no(prompt: str, default: str = None) -> str:
|
||||
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 = "y"
|
||||
|
||||
return CharReader.get_validated_char_input(
|
||||
prompt=prompt,
|
||||
valid_keys=["y", "n"],
|
||||
default=default,
|
||||
error_message="Invalid input '{response}'. Please enter 'y' or 'n'.",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_cicd_choice(prompt: str, valid_keys: list[str], default: str = "n") -> str:
|
||||
valid_keys_str = ", ".join(f"'{k}'" for k in valid_keys)
|
||||
return CharReader.get_validated_char_input(
|
||||
prompt=prompt,
|
||||
valid_keys=valid_keys,
|
||||
default=default,
|
||||
error_message=f"Invalid input '{{response}}'. Please enter one of: {valid_keys_str}.",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_structure_choice() -> str:
|
||||
"""Get structure type choice without requiring Enter key."""
|
||||
return CharReader.get_validated_char_input(
|
||||
prompt=f"[{UIColors.HIGHLIGHT}]Choose structure type (1/2/3/4) [default: 1]:[/{UIColors.HIGHLIGHT}]",
|
||||
valid_keys=["1", "2", "3", "4"],
|
||||
default="1",
|
||||
error_message="Invalid input '{response}'. Please enter '1', '2', '3', or '4'.",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _get_char_windows() -> str:
|
||||
import msvcrt
|
||||
|
||||
encodings = ["utf-8", "latin1"]
|
||||
|
||||
for encoding in encodings:
|
||||
try:
|
||||
ch = msvcrt.getch()
|
||||
if isinstance(ch, bytes):
|
||||
if ch == b"\x03":
|
||||
raise KeyboardInterrupt
|
||||
decoded = ch.decode(encoding).lower()
|
||||
else:
|
||||
if ord(ch) == 3: # Ctrl+C
|
||||
raise KeyboardInterrupt
|
||||
decoded = ch.lower()
|
||||
return decoded
|
||||
except (UnicodeDecodeError, AttributeError):
|
||||
continue
|
||||
|
||||
# Fallback
|
||||
try:
|
||||
ch = msvcrt.getch()
|
||||
if isinstance(ch, bytes):
|
||||
if ch == b"\x03":
|
||||
raise KeyboardInterrupt
|
||||
return ch.decode("utf-8", errors="ignore").lower()
|
||||
else:
|
||||
if ord(ch) == 3: # Ctrl+C
|
||||
raise KeyboardInterrupt
|
||||
return ch.lower()
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def _get_char_unix() -> str:
|
||||
fd = sys.stdin.fileno()
|
||||
old_settings = termios.tcgetattr(fd)
|
||||
try:
|
||||
tty.setraw(sys.stdin.fileno())
|
||||
ch = sys.stdin.read(1)
|
||||
if ord(ch) == 3: # Ctrl+C
|
||||
raise KeyboardInterrupt
|
||||
return ch.lower()
|
||||
finally:
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
||||
|
||||
|
||||
def get_user_input() -> Tuple[str, str, str, list, dict]:
|
||||
try:
|
||||
collector = InputCollector()
|
||||
console.print()
|
||||
|
||||
UIFormatter.print_separator()
|
||||
console.print(f"\n[{UIColors.INFO}]Choose Structure Type[/{UIColors.INFO}]\n")
|
||||
console.print(f"[{UIColors.MUTED}]1. Standard structure (default Django layout)[/{UIColors.MUTED}]")
|
||||
console.print(f"[{UIColors.MUTED}]2. Predefined structure (apps/users, apps/core, api/)[/{UIColors.MUTED}]")
|
||||
console.print(f"[{UIColors.MUTED}]3. Unified structure (core/, apps/core, apps/api)[/{UIColors.MUTED}]")
|
||||
console.print(f"[{UIColors.MUTED}]4. Single folder layout (everything in one folder)[/{UIColors.MUTED}]")
|
||||
console.print()
|
||||
structure_choices = [
|
||||
("Standard structure (default Django layout)", "1"),
|
||||
("Predefined structure (apps/users, apps/core, api/)", "2"),
|
||||
("Unified structure (core/, apps/core, apps/api)", "3"),
|
||||
("Single folder layout (everything in one folder)", "4"),
|
||||
]
|
||||
|
||||
structure_choice = CharReader.get_structure_choice()
|
||||
structure_choice = collector._get_selection("Choose structure type:", structure_choices, default="1")
|
||||
|
||||
use_standard = structure_choice == "1"
|
||||
use_predefined = structure_choice == "2"
|
||||
@@ -463,26 +221,20 @@ def get_user_input() -> Tuple[str, str, str, list, dict]:
|
||||
use_single = structure_choice == "4"
|
||||
|
||||
# Step 1: Project Setup
|
||||
UIFormatter.print_separator()
|
||||
console.print(f"\n[{UIColors.INFO}]Step 1: Project Setup[/{UIColors.INFO}]\n")
|
||||
console.print(f"[{UIColors.MUTED}]Press Enter or enter '.' to create in current directory[/{UIColors.MUTED}]")
|
||||
|
||||
project_dir = collector._get_project_directory()
|
||||
if project_dir is None:
|
||||
UIFormatter.print_error("Maximum attempts reached for project directory name. Exiting.")
|
||||
# Should be handled by questionary loop, but just in case
|
||||
sys.exit(1)
|
||||
|
||||
project_name = project_dir
|
||||
single_module_name = None
|
||||
|
||||
if use_standard:
|
||||
console.print()
|
||||
console.print(f"[{UIColors.MUTED}]Common names: config, core, settings[/{UIColors.MUTED}]")
|
||||
project_name = collector.get_validated_input(
|
||||
"Enter Django project name", validate_project_name, "Django project name"
|
||||
)
|
||||
elif use_single:
|
||||
console.print()
|
||||
console.print(f"[{UIColors.MUTED}]Common names: project, core, app[/{UIColors.MUTED}]")
|
||||
single_module_name = (
|
||||
collector.get_validated_input(
|
||||
@@ -495,9 +247,6 @@ def get_user_input() -> Tuple[str, str, str, list, dict]:
|
||||
)
|
||||
|
||||
# Step 2: Database Configuration
|
||||
console.print()
|
||||
UIFormatter.print_separator()
|
||||
console.print(f"\n[{UIColors.INFO}]Step 2: Database Configuration[/{UIColors.INFO}]")
|
||||
database_type = collector.get_database_type_choice()
|
||||
use_database_url = collector.get_database_config_choice()
|
||||
|
||||
@@ -507,15 +256,10 @@ def get_user_input() -> Tuple[str, str, str, list, dict]:
|
||||
app_names = []
|
||||
|
||||
if use_standard:
|
||||
console.print()
|
||||
UIFormatter.print_separator()
|
||||
console.print(f"\n[{UIColors.INFO}]Step 3: Django Apps[/{UIColors.INFO}]\n")
|
||||
nested, nested_dir = collector.get_nested_apps_config()
|
||||
app_names = collector.get_app_names()
|
||||
|
||||
# Step 3/4: CI/CD Pipeline
|
||||
step_num = 4 if use_standard else 3
|
||||
console.print(f"\n[{UIColors.INFO}]Step {step_num}: CI/CD Pipeline[/{UIColors.INFO}]")
|
||||
use_github, use_gitlab = collector.get_cicd_choice()
|
||||
|
||||
# Generate Metadata
|
||||
@@ -532,8 +276,8 @@ def get_user_input() -> Tuple[str, str, str, list, dict]:
|
||||
)
|
||||
return project_dir, project_name, app_names[0], app_names, metadata.to_dict()
|
||||
else:
|
||||
project_name, primary_app, app_names, metadata_dict = collector._get_structure_metadata(
|
||||
project_dir,
|
||||
options = StructureOptions(
|
||||
project_dir=project_dir,
|
||||
predefined=use_predefined,
|
||||
unified=use_unified,
|
||||
single=use_single,
|
||||
@@ -543,6 +287,7 @@ def get_user_input() -> Tuple[str, str, str, list, dict]:
|
||||
use_github=use_github,
|
||||
use_gitlab=use_gitlab,
|
||||
)
|
||||
project_name, primary_app, app_names, metadata_dict = collector._get_structure_metadata(options)
|
||||
return project_dir, project_name, primary_app, app_names, metadata_dict
|
||||
|
||||
except KeyboardInterrupt:
|
||||
@@ -576,8 +321,7 @@ def confirm_setup(project_dir: str, project_name: str, app_names: list, metadata
|
||||
console.print()
|
||||
|
||||
try:
|
||||
response = CharReader.get_yes_no(f"[{UIColors.WARNING}]Proceed with setup? (y/N):[/{UIColors.WARNING}]")
|
||||
return response == "y"
|
||||
return UIFormatter.confirm("Proceed with setup?", default=True)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
UIFormatter.print_info("\nSetup cancelled by user.")
|
||||
@@ -591,7 +335,3 @@ def _get_cicd_display(metadata: dict) -> str:
|
||||
if metadata.get("use_gitlab_ci", False):
|
||||
choices.append("GitLab CI")
|
||||
return ", ".join(choices) if choices else "None"
|
||||
|
||||
|
||||
get_char = CharReader.get_char
|
||||
_get_validated_input = InputCollector().get_validated_input
|
||||
@@ -278,3 +278,25 @@ def detect_nested_structure_from_settings(
|
||||
pass
|
||||
|
||||
return False, None, search_dir
|
||||
|
||||
|
||||
def get_djinit_config(project_root: str = None) -> dict | None:
|
||||
"""Read .djinit configuration file."""
|
||||
import json
|
||||
|
||||
if project_root is None:
|
||||
project_root = os.getcwd()
|
||||
|
||||
config_path = os.path.join(project_root, ".djinit")
|
||||
if not os.path.exists(config_path):
|
||||
# Try finding it in parent directories
|
||||
parent = os.path.dirname(project_root)
|
||||
if parent and parent != project_root:
|
||||
return get_djinit_config(parent)
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(config_path) as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
return None
|
||||
@@ -8,11 +8,7 @@ import secrets
|
||||
import string
|
||||
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.table import Table
|
||||
from rich.text import Text
|
||||
|
||||
from djinit.ui.console import UIColors
|
||||
|
||||
console = Console()
|
||||
|
||||
@@ -38,22 +34,6 @@ def display_secret_keys(keys: list[str]) -> None:
|
||||
console.print()
|
||||
|
||||
|
||||
def generate_secret_command():
|
||||
if __name__ == "__main__":
|
||||
keys = generate_multiple_keys(3, 50)
|
||||
display_secret_keys(keys)
|
||||
|
||||
instructions = Text()
|
||||
instructions.append("📋 Usage Instructions:\n", style=UIColors.ACCENT)
|
||||
instructions.append("1. Copy the appropriate secret key for your environment\n")
|
||||
instructions.append("2. Add it to your .env file:\n")
|
||||
instructions.append(" SECRET_KEY=your_secret_key_here\n", style=UIColors.CODE)
|
||||
instructions.append("3. Or set it as an environment variable:\n")
|
||||
instructions.append(" export SECRET_KEY=your_secret_key_here\n", style=UIColors.CODE)
|
||||
instructions.append("4. Never commit secret keys to version control!\n", style=UIColors.WARNING)
|
||||
|
||||
console.print(Panel(instructions, title="💡 How to Use", border_style="blue"))
|
||||
console.print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
generate_secret_command()
|
||||
+9
-5
@@ -7,10 +7,10 @@ packages = ["djinit"]
|
||||
|
||||
[project]
|
||||
name = "djinitx"
|
||||
version = "0.3.0"
|
||||
version = "0.3.2"
|
||||
description = "A CLI tool to setup Django project"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
requires-python = ">=3.10"
|
||||
authors = [
|
||||
{ name = "Sankalp Tharu", email = "sankalptharu50028@gmail.com" }
|
||||
]
|
||||
@@ -47,11 +47,9 @@ classifiers=[
|
||||
"Topic :: Software Development :: Code Generators",
|
||||
]
|
||||
dependencies = [
|
||||
"click>=8.3.0",
|
||||
"rich>=14.2.0",
|
||||
"ruff>=0.14.2",
|
||||
"jinja2>=3.1.0",
|
||||
"pre-commit>=4.5.0",
|
||||
"questionary>=2.0.1",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
@@ -60,3 +58,9 @@ Homepage = "https://github.com/S4NKALP/djinit"
|
||||
[project.scripts]
|
||||
djinit = "djinit.generator:main"
|
||||
dj = "djinit.generator:main"
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pre-commit>=4.5.0",
|
||||
"ruff>=0.14.2",
|
||||
]
|
||||
+6
-3
@@ -1,4 +1,7 @@
|
||||
click==8.3.1
|
||||
jinja2==3.1.6
|
||||
questionary==2.1.1
|
||||
rich==14.2.0
|
||||
ruff==0.14.6
|
||||
jinja2>=3.1.0
|
||||
|
||||
# dev
|
||||
ruff==0.14.2
|
||||
pre-commit==4.5.0
|
||||
@@ -2,7 +2,7 @@ from setuptools import find_packages, setup
|
||||
|
||||
setup(
|
||||
name="djinitx",
|
||||
version="0.3.0",
|
||||
version="0.3.2",
|
||||
description="A CLI tool to setup Django projects",
|
||||
long_description=open("README.md").read(),
|
||||
long_description_content_type="text/markdown",
|
||||
@@ -16,6 +16,7 @@ setup(
|
||||
"click>=8.3.0",
|
||||
"rich>=14.2.0",
|
||||
"jinja2>=3.1.0",
|
||||
"questionary>=2.0.1",
|
||||
],
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.13"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
[[package]]
|
||||
name = "cfgv"
|
||||
@@ -11,27 +11,6 @@ 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"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
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"
|
||||
@@ -43,22 +22,30 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "djinitx"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "pre-commit" },
|
||||
{ name = "questionary" },
|
||||
{ name = "rich" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "pre-commit" },
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "click", specifier = ">=8.3.0" },
|
||||
{ name = "jinja2", specifier = ">=3.1.0" },
|
||||
{ name = "pre-commit", specifier = ">=4.5.0" },
|
||||
{ name = "questionary", specifier = ">=2.0.1" },
|
||||
{ name = "rich", specifier = ">=14.2.0" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "pre-commit", specifier = ">=4.5.0" },
|
||||
{ name = "ruff", specifier = ">=0.14.2" },
|
||||
]
|
||||
|
||||
@@ -110,6 +97,39 @@ version = "3.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
|
||||
@@ -199,6 +219,18 @@ 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 = "prompt-toolkit"
|
||||
version = "3.0.52"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "wcwidth" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.2"
|
||||
@@ -214,6 +246,34 @@ 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/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
|
||||
{ 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" },
|
||||
@@ -244,6 +304,18 @@ wheels = [
|
||||
{ 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 = "questionary"
|
||||
version = "2.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "prompt-toolkit" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f6/45/eafb0bba0f9988f6a2520f9ca2df2c82ddfa8d67c95d6625452e97b204a5/questionary-2.1.1.tar.gz", hash = "sha256:3d7e980292bb0107abaa79c68dd3eee3c561b83a0f89ae482860b181c8bd412d", size = 25845, upload-time = "2025-08-28T19:00:20.851Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59", size = 36753, upload-time = "2025-08-28T19:00:19.56Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "14.2.0"
|
||||
@@ -283,6 +355,15 @@ wheels = [
|
||||
{ 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 = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "virtualenv"
|
||||
version = "20.35.4"
|
||||
@@ -291,8 +372,18 @@ dependencies = [
|
||||
{ name = "distlib" },
|
||||
{ name = "filelock" },
|
||||
{ name = "platformdirs" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
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" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wcwidth"
|
||||
version = "0.2.14"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" },
|
||||
]
|
||||
Reference in new issue
Block a user