13 Commits
Author SHA1 Message Date
sankalp f7c5a99e41 chore: bump version to 0.3.2 and remove click dependency 2025-12-06 19:35:42 +05:45
sankalp 68f8cb81b5 chore: move dev dependencies to a separate group 2025-12-06 19:34:14 +05:45
sankalp 962fa3c504 chore: add questionary dependency for interactive prompts 2025-12-05 19:53:03 +05:45
sankalp f875893399 Merge branch 'main' of github.com:S4NKALP/djinit 2025-12-04 16:14:12 +05:45
sankalp cd1054b071 chore: bump version to 0.3.1 2025-12-04 16:13:16 +05:45
sankalp e60f5a4c1b Merge pull request #15 from S4NKALP/dependabot/pip/main/ruff-0.14.7
chore(deps): bump ruff from 0.14.6 to 0.14.7
2025-12-02 07:53:09 +05:45
sankalp 4d45581419 Merge pull request #14 from S4NKALP/dependabot/github_actions/main/actions/checkout-6
chore(deps): bump actions/checkout from 4 to 6
2025-12-02 07:52:56 +05:45
sankalp bde87b91a0 Merge pull request #13 from S4NKALP/dependabot/github_actions/main/actions/setup-python-6
chore(deps): bump actions/setup-python from 4 to 6
2025-12-02 07:52:42 +05:45
dependabot[bot] f008b08e8b chore(deps): bump ruff from 0.14.6 to 0.14.7
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.14.6 to 0.14.7.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.14.6...0.14.7)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.14.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 03:15:35 +00:00
dependabot[bot] 8d67e7831c chore(deps): bump actions/checkout from 4 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 03:01:07 +00:00
dependabot[bot] 717fe0aec5 chore(deps): bump actions/setup-python from 4 to 6
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 6.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 03:01:00 +00:00
sankalp 00fa9ae5f5 chore: update uv.lock to require Python 3.10+ 2025-11-29 20:13:17 +05:45
sankalp 56d54c5f37 feat: improve project structure detection and interactive configuration 2025-11-29 20:10:30 +05:45
15 changed files with 463 additions and 563 deletions

No files matched your search

+2 -2
View File
@@ -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'
+2 -1
View File
@@ -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
View File
@@ -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
+7 -17
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+22
View File
@@ -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
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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": [
Generated
+118 -27
View File
@@ -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" },
]