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: permissions:
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v6
with: with:
python-version: '3.12' python-version: '3.12'
cache: 'pip' cache: 'pip'
+2 -1
View File
@@ -10,5 +10,6 @@
## Enhancements ## Enhancements
- [ ] Add more project structure templates - [ ] Add more project structure templates
- [ ] Add interactive configuration wizard - [x] Add interactive configuration wizard
- [ ] Add testing framework setup (pytest, coverage) - [ ] 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.files import FileManager
from djinit.services.project import ProjectManager from djinit.services.project import ProjectManager
from djinit.ui.console import UIFormatter from djinit.ui.console import UIFormatter
from djinit.utils.security import generate_secret_command
class Cli: class Cli:
@@ -27,7 +26,8 @@ class Cli:
self.project_manager = ProjectManager(project_dir, project_name, app_names, metadata) self.project_manager = ProjectManager(project_dir, project_name, app_names, metadata)
self.file_manager = FileManager(self.project_root, 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"): if self.metadata.get("predefined_structure"):
self.metadata.setdefault("project_module_name", "config") self.metadata.setdefault("project_module_name", "config")
self.metadata.setdefault("nested_apps", True) self.metadata.setdefault("nested_apps", True)
@@ -36,7 +36,7 @@ class Cli:
self.app_names = ["users", "core"] self.app_names = ["users", "core"]
self.project_manager.app_names = self.app_names 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("project_module_name", "core")
self.metadata.setdefault("nested_apps", True) self.metadata.setdefault("nested_apps", True)
self.metadata.setdefault("nested_dir", "apps") self.metadata.setdefault("nested_dir", "apps")
@@ -44,12 +44,15 @@ class Cli:
self.app_names = [] self.app_names = []
self.project_manager.app_names = 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) self.metadata.setdefault("nested_apps", False)
if not self.app_names: if not self.app_names:
self.app_names = [] self.app_names = []
self.project_manager.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: List[Tuple[str, Callable[[], None]]] = []
steps.append(("Creating Django project", self.project_manager.create_project)) 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_requirements,
self.file_manager.create_readme, self.file_manager.create_readme,
self.file_manager.create_env_file, self.file_manager.create_env_file,
self.file_manager.create_djinit_config,
lambda: self.file_manager.create_pyproject_toml(self.metadata), lambda: self.file_manager.create_pyproject_toml(self.metadata),
] ]
@@ -114,7 +118,3 @@ class Cli:
if self.metadata.get("use_gitlab_ci", True): if self.metadata.get("use_gitlab_ci", True):
self.file_manager.create_gitlab_ci() 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.services.app import AppManager
from djinit.ui.console import UIColors, UIFormatter, console 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 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_header("Django App Creation")
UIFormatter.print_info("") UIFormatter.print_info("")
tokens = args.app_name if isinstance(args.app_name, list) else [args.app_name] raw_tokens = args.app_name if isinstance(args.app_name, list) else [args.app_name]
pieces = []
for token in tokens: # Flatten and clean tokens
if token is None: app_names = [part.strip() for token in raw_tokens if token for part in str(token).split(",") if part.strip()]
continue
for part in str(token).split(","):
part = part.strip()
if part:
pieces.append(part)
app_names = pieces
if not app_names: if not app_names:
UIFormatter.print_error("App name cannot be empty") UIFormatter.print_error("App name cannot be empty")
sys.exit(1) sys.exit(1)
seen = set() # Deduplicate while preserving order
deduped_names = [] deduped_names = list(dict.fromkeys(app_names))
for name in app_names:
if name not in seen:
deduped_names.append(name)
seen.add(name)
for name in deduped_names: for name in deduped_names:
is_valid, error_msg = validate_app_name(name) is_valid, error_msg = validate_app_name(name)
+37 -13
View File
@@ -16,6 +16,7 @@ from djinit.utils.common import (
extract_existing_apps, extract_existing_apps,
find_project_dir, find_project_dir,
find_settings_path, find_settings_path,
get_djinit_config,
insert_apps_into_user_defined_apps, insert_apps_into_user_defined_apps,
is_django_project, is_django_project,
) )
@@ -27,6 +28,7 @@ class AppManager:
self.app_name = app_name self.app_name = app_name
self.current_dir = os.getcwd() self.current_dir = os.getcwd()
self.manage_py_path = os.path.join(self.current_dir, "manage.py") 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 self._project_structure_cache = None
def create_app(self) -> bool: def create_app(self) -> bool:
@@ -126,36 +128,58 @@ class AppManager:
def _get_project_structure(self) -> tuple[bool, Optional[str], str]: def _get_project_structure(self) -> tuple[bool, Optional[str], str]:
"""Get project structure with caching to avoid repeated detection.""" """Get project structure with caching to avoid repeated detection."""
if self._project_structure_cache is None: 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: # Determine base dir for apps
self._project_structure_cache = (False, None, self.current_dir) 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: else:
self._project_structure_cache = detect_nested_structure_from_settings( # Fallback to old detection method
settings_base_path, self.current_dir 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 return self._project_structure_cache
def _is_predefined_structure(self) -> bool: 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") apps_dir = os.path.join(self.current_dir, "apps")
api_dir = os.path.join(self.current_dir, "api") api_dir = os.path.join(self.current_dir, "api")
return os.path.isdir(apps_dir) and os.path.isdir(api_dir) return os.path.isdir(apps_dir) and os.path.isdir(api_dir)
def _is_restricted_structure(self) -> bool: def _is_restricted_structure(self) -> bool:
"""Check if the project structure is Unified or Single Folder.""" """Check if the project structure is Unified or Single Folder."""
apps_api_dir = os.path.join(self.current_dir, "apps", "api") if self.config:
if os.path.isdir(apps_api_dir): 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 return True
apps_dir = os.path.join(self.current_dir, "apps") # Check for Predefined structure (apps/ exists but not apps/api)
if os.path.isdir(apps_dir): if os.path.isdir(os.path.join(self.current_dir, "apps")):
return False return False
# Check for Single Folder structure (models/ and api/ exist in project dir)
project_dir, _ = find_project_dir(self.current_dir) project_dir, _ = find_project_dir(self.current_dir)
if project_dir: if project_dir:
models_dir = os.path.join(project_dir, "models") has_models = os.path.isdir(os.path.join(project_dir, "models"))
api_dir = os.path.join(project_dir, "api") has_api = os.path.isdir(os.path.join(project_dir, "api"))
if os.path.isdir(models_dir) and os.path.isdir(api_dir): if has_models and has_api:
return True return True
return False 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: 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.""" """Helper to create multiple folders and files from a specification dict."""
base_context = base_context or {} 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") 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) file_path = os.path.join(folder_path, filename)
create_file_from_template(file_path, template_path, base_context, f"Created {file_path}") 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" message = f"Created {prefix}{subdir}/__init__.py" if prefix else f"Created {subdir}/__init__.py"
create_directory_with_init(subdir_path, message) 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: def create_gitignore(self) -> None:
self._render_and_create_file(".gitignore", "project/gitignore.j2", {}, "Created .gitignore file") 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") settings_dir = os.path.join(core_dir, "settings")
create_directory_with_init(settings_dir, "Created core/settings/__init__.py") create_directory_with_init(settings_dir, "Created core/settings/__init__.py")
from djinit.utils.security import generate_secret_key # Create settings files
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.
base_context = { base_context = {
"project_name": "core", # Project module is 'core' "project_name": "core",
"app_names": ["apps"], # 'apps' is the main app "app_names": ["apps"],
"use_database_url": self.metadata.get("use_database_url", True), "use_database_url": self.metadata.get("use_database_url", True),
"database_type": self.metadata.get("database_type", "postgresql"), "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 [ # Create lifecycle files (urls, wsgi, asgi)
("base.py", base_context), self._create_lifecycle_files(core_dir, "core", api_module="apps.api")
("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)
# 2. Create 'apps' directory (The Main App) # 2. Create 'apps' directory (The Main App)
apps_dir = os.path.join(self.project_root, "apps") apps_dir = os.path.join(self.project_root, "apps")
@@ -287,43 +309,19 @@ class FileManager:
settings_dir = os.path.join(project_dir, "settings") settings_dir = os.path.join(project_dir, "settings")
create_directory_with_init(settings_dir, f"Created {self.module_name}/settings/__init__.py") create_directory_with_init(settings_dir, f"Created {self.module_name}/settings/__init__.py")
from djinit.utils.security import generate_secret_key # Create settings files
secret_key = generate_secret_key()
base_context = { base_context = {
"project_name": self.module_name, "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), "use_database_url": self.metadata.get("use_database_url", True),
"database_type": self.metadata.get("database_type", "postgresql"), "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 [ # Create lifecycle files (urls, wsgi, asgi)
("base.py", base_context), self._create_lifecycle_files(
("development.py", dev_context), project_dir, self.module_name, api_module=f"{self.module_name}.api", comment_out_api=True
("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
)
components = ["admin", "api", "models", "tests"] components = ["admin", "api", "models", "tests"]
self._create_subdirectories_with_init(project_dir, components, f"{self.module_name}/") self._create_subdirectories_with_init(project_dir, components, f"{self.module_name}/")
@@ -395,3 +393,37 @@ class FileManager:
context, context,
"Created GitLab CI configuration (.gitlab-ci.yml)", "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}") UIFormatter.print_success(f"Added apps to USER_DEFINED_APPS: {added_apps_str}")
def validate_project_structure(self) -> None: def validate_project_structure(self) -> None:
join = lambda *args: os.path.join(self.project_root, *args)
required_files = [ required_files = [
os.path.join(self.project_root, "manage.py"), join("manage.py"),
os.path.join(self.project_root, self.module_name, "__init__.py"), join(self.module_name, "__init__.py"),
os.path.join(self.project_root, self.module_name, "settings", "__init__.py"), join(self.module_name, "settings", "__init__.py"),
os.path.join(self.project_root, self.module_name, "settings", "base.py"), join(self.module_name, "settings", "base.py"),
os.path.join(self.project_root, self.module_name, "settings", "development.py"), join(self.module_name, "settings", "development.py"),
os.path.join(self.project_root, self.module_name, "settings", "production.py"), join(self.module_name, "settings", "production.py"),
os.path.join(self.project_root, self.module_name, "urls.py"), join(self.module_name, "urls.py"),
os.path.join(self.project_root, self.module_name, "wsgi.py"), join(self.module_name, "wsgi.py"),
os.path.join(self.project_root, self.module_name, "asgi.py"), join(self.module_name, "asgi.py"),
] ]
apps_base_dir = self._get_apps_base_dir() apps_base_dir = self._get_apps_base_dir()
if self.metadata.get("unified_structure"): if self.metadata.get("unified_structure"):
# Validate Unified Structure # Validate Unified Structure
apps_dir = os.path.join(self.project_root, "apps") apps_dir = join("apps")
unified_files = [ unified_files = [
os.path.join(apps_dir, "__init__.py"), os.path.join(apps_dir, "__init__.py"),
os.path.join(apps_dir, "apps.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"): elif not self.metadata.get("predefined_structure") and not self.metadata.get("single_structure"):
for app_name in self.app_names: for app_name in self.app_names:
app_path = lambda f, app=app_name: os.path.join(apps_base_dir, app, f)
app_files = [ app_files = [
os.path.join(apps_base_dir, app_name, "__init__.py"), app_path("__init__.py"),
os.path.join(apps_base_dir, app_name, "apps.py"), app_path("apps.py"),
os.path.join(apps_base_dir, app_name, "models.py"), app_path("models.py"),
os.path.join(apps_base_dir, app_name, "views.py"), app_path("views.py"),
os.path.join(apps_base_dir, app_name, "serializers.py"), app_path("serializers.py"),
os.path.join(apps_base_dir, app_name, "routes.py"), app_path("routes.py"),
os.path.join(apps_base_dir, app_name, "tests.py"), app_path("tests.py"),
os.path.join(apps_base_dir, app_name, "migrations"), app_path("migrations"),
os.path.join(apps_base_dir, app_name, "admin.py"), app_path("admin.py"),
] ]
required_files.extend(app_files) 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 contextlib import contextmanager
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
import questionary
from rich import box from rich import box
from rich.console import Console from rich.console import Console
from rich.panel import Panel from rich.panel import Panel
@@ -164,20 +165,28 @@ class UIFormatter:
@staticmethod @staticmethod
def confirm(prompt: str, default: bool = True) -> bool: def confirm(prompt: str, default: bool = True) -> bool:
"""Display a confirmation prompt""" """Display a confirmation prompt as a selectable list"""
default_str = "Y/n" if default else "y/N" choices = [
response = console.input(f"[bold cyan]?[/bold cyan] {prompt} [{default_str}]: ").strip().lower() questionary.Choice("Yes", value=True),
if not response: questionary.Choice("No", value=False),
return default ]
return response in ("y", "yes") 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 @staticmethod
def prompt(message: str, default: Optional[str] = None) -> str: def prompt(message: str, default: Optional[str] = None) -> str:
"""Display an input prompt""" """Display an input prompt"""
if default: return questionary.text(message, default=default or "").ask()
message = f"{message} [{default}]"
response = console.input(f"[bold cyan]?[/bold cyan] {message}: ").strip()
return response or default or ""
@staticmethod @staticmethod
def handle_exception(e: Exception): 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 os
import sys import sys
import termios from typing import Tuple, TypedDict
import tty
from enum import Enum import questionary
from typing import Callable, Tuple
from djinit.core.types import ProjectMetadata from djinit.core.types import ProjectMetadata
from djinit.ui.console import UIColors, UIFormatter, console 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 from djinit.utils.validators import validate_app_name, validate_project_name
class CICDOption(Enum): class StructureOptions(TypedDict):
GITHUB = ("g", "GitHub Actions only") project_dir: str
GITLAB = ("l", "GitLab CI only") predefined: bool
BOTH = ("b", "Both (GitHub Actions + GitLab CI)") unified: bool
NONE = ("n", "None (skip CI/CD)") single: bool
single_module_name: str | None
def __init__(self, key: str, description: str): database_type: str
self.key = key use_database_url: bool
self.description = description use_github: bool
use_gitlab: bool
class InputCollector: class InputCollector:
MAX_ATTEMPTS = 3
def __init__(self): def __init__(self):
pass pass
def get_validated_input( def get_validated_input(self, prompt: str, validator: callable, input_type: str, allow_empty: bool = False) -> str:
self, prompt: str, validator: Callable[[str], Tuple[bool, str]], input_type: str, allow_empty: bool = False def q_validator(text):
) -> str: if not text and allow_empty:
attempt = 0 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: result = questionary.text(prompt, validate=q_validator).ask()
try: if result is None:
user_input = console.input(f"[{UIColors.HIGHLIGHT}]{prompt}:[/{UIColors.HIGHLIGHT}] ") raise KeyboardInterrupt
return result
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}]")
def get_app_names(self) -> list[str]: def get_app_names(self) -> list[str]:
console.print( user_input = questionary.text(
f"[{UIColors.MUTED}]Enter app names separated by commas(e.g. users, products, orders)[/{UIColors.MUTED}]" "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( if user_input is None:
f"[{UIColors.HIGHLIGHT}]Enter app names (comma-separated or single):[/{UIColors.HIGHLIGHT}] " raise KeyboardInterrupt
)
if not user_input.strip():
UIFormatter.print_error("At least one app name is required")
return self.get_app_names()
if "," in user_input: if "," in user_input:
return self._parse_comma_separated_apps(user_input) return self._parse_comma_separated_apps(user_input)
@@ -120,76 +88,69 @@ class InputCollector:
return app_list return app_list
def _get_project_directory(self) -> str | None: 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: is_valid, error_msg = validate_project_name(text)
try: if not is_valid:
user_input = console.input( return error_msg
f"[{UIColors.HIGHLIGHT}]Enter project directory name:[/{UIColors.HIGHLIGHT}] "
).strip()
if not user_input or user_input == ".": if os.path.exists(text):
UIFormatter.print_info(f"Creating project in current directory: {os.getcwd()}") return f"Directory '{text}' already exists."
return "."
is_valid, error_msg = validate_project_name(user_input) return True
if not is_valid:
UIFormatter.print_error(error_msg)
attempt += 1
self._show_retry_message(attempt)
continue
if os.path.exists(user_input): project_dir = questionary.text("Enter project directory name:", validate=validate_dir).ask()
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
return user_input if project_dir is None:
raise KeyboardInterrupt
except KeyboardInterrupt: if not project_dir or project_dir == ".":
UIFormatter.print_info("\nSetup cancelled by user.") UIFormatter.print_info(f"Creating project in current directory: {os.getcwd()}")
sys.exit(0) 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]: def get_cicd_choice(self) -> Tuple[bool, bool]:
UIFormatter.print_separator() choices = [
console.print(f"\n[{UIColors.INFO}]CI/CD Pipeline[/{UIColors.INFO}]\n") ("None (skip CI/CD)", "none"),
console.print() ("GitHub Actions only", "github"),
("GitLab CI only", "gitlab"),
("Both (GitHub Actions + GitLab CI)", "both"),
]
for option in CICDOption: choice = self._get_selection("Select CI/CD pipeline:", choices, default="none")
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_map = { choice_map = {
CICDOption.BOTH.key: (True, True), "both": (True, True),
CICDOption.GITHUB.key: (True, False), "github": (True, False),
CICDOption.GITLAB.key: (False, True), "gitlab": (False, True),
"none": (False, False),
} }
return choice_map.get(choice, (False, False)) return choice_map.get(choice, (False, False))
def get_nested_apps_config(self) -> Tuple[bool, str | None]: def get_nested_apps_config(self) -> Tuple[bool, str | None]:
UIFormatter.print_separator() choice = UIFormatter.confirm(
console.print( "Do you want to place apps inside a package directory (e.g. 'src/')?", default=False
f"[{UIColors.MUTED}]Do you want to place apps inside a package directory (e.g. 'src/')?[/{UIColors.MUTED}]"
) )
console.print()
choice = CharReader.get_yes_no(f"[{UIColors.HIGHLIGHT}]Nested Django apps? (y/N):[/{UIColors.HIGHLIGHT}]") if not choice:
if choice != "y":
return False, None return False, None
dir_name = self.get_validated_input( dir_name = self.get_validated_input(
@@ -200,262 +161,59 @@ class InputCollector:
return True, dir_name return True, dir_name
def get_database_config_choice(self) -> bool: def get_database_config_choice(self) -> bool:
UIFormatter.print_separator() return UIFormatter.confirm("Use DATABASE_URL? (recommended for production)", default=True)
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"
def get_database_type_choice(self) -> str: def get_database_type_choice(self) -> str:
console.print() choices = [
console.print(f"[{UIColors.INFO}]Select Database Type[/{UIColors.INFO}]\n") ("PostgreSQL", "postgresql"),
console.print(f" [{UIColors.SUCCESS}]1[/{UIColors.SUCCESS}] PostgreSQL") ("MySQL", "mysql"),
console.print(f" [{UIColors.SUCCESS}]2[/{UIColors.SUCCESS}] MySQL") ]
console.print()
choice = CharReader.get_validated_char_input( return self._get_selection("Choose database:", choices, default="postgresql")
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 "mysql" if choice == "2" else "postgresql" def _get_structure_metadata(self, options: StructureOptions) -> Tuple[str, str, list[str], dict]:
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]:
"""Helper method to generate metadata dictionary.""" """Helper method to generate metadata dictionary."""
project_dir = options["project_dir"]
project_name = project_dir project_name = project_dir
app_names: list[str] = [] app_names: list[str] = []
# Default package_name to "backend" if project_dir is "." or empty # Default package_name to "backend" if project_dir is "." or empty
package_name = get_package_name(project_dir) 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: if options["single"]:
project_module_name = single_module_name or "project" project_module_name = options["single_module_name"] or "project"
metadata = ProjectMetadata( metadata = ProjectMetadata(
package_name=package_name, package_name=package_name,
use_github_actions=use_github, use_github_actions=options["use_github"],
use_gitlab_ci=use_gitlab, use_gitlab_ci=options["use_gitlab"],
nested_apps=True, nested_apps=not options["single"],
nested_dir="apps", nested_dir="apps" if not options["single"] else None,
use_database_url=use_database_url, use_database_url=options["use_database_url"],
database_type=database_type, database_type=options["database_type"],
predefined_structure=predefined, predefined_structure=options["predefined"],
unified_structure=unified, unified_structure=options["unified"],
single_structure=single, single_structure=options["single"],
project_module_name=project_module_name, project_module_name=project_module_name,
) )
return project_name, "", app_names, metadata.to_dict() 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]: def get_user_input() -> Tuple[str, str, str, list, dict]:
try: try:
collector = InputCollector() collector = InputCollector()
console.print()
UIFormatter.print_separator() structure_choices = [
console.print(f"\n[{UIColors.INFO}]Choose Structure Type[/{UIColors.INFO}]\n") ("Standard structure (default Django layout)", "1"),
console.print(f"[{UIColors.MUTED}]1. Standard structure (default Django layout)[/{UIColors.MUTED}]") ("Predefined structure (apps/users, apps/core, api/)", "2"),
console.print(f"[{UIColors.MUTED}]2. Predefined structure (apps/users, apps/core, api/)[/{UIColors.MUTED}]") ("Unified structure (core/, apps/core, apps/api)", "3"),
console.print(f"[{UIColors.MUTED}]3. Unified structure (core/, apps/core, apps/api)[/{UIColors.MUTED}]") ("Single folder layout (everything in one folder)", "4"),
console.print(f"[{UIColors.MUTED}]4. Single folder layout (everything in one folder)[/{UIColors.MUTED}]") ]
console.print()
structure_choice = CharReader.get_structure_choice() structure_choice = collector._get_selection("Choose structure type:", structure_choices, default="1")
use_standard = structure_choice == "1" use_standard = structure_choice == "1"
use_predefined = structure_choice == "2" use_predefined = structure_choice == "2"
@@ -463,26 +221,20 @@ def get_user_input() -> Tuple[str, str, str, list, dict]:
use_single = structure_choice == "4" use_single = structure_choice == "4"
# Step 1: Project Setup # 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() project_dir = collector._get_project_directory()
if project_dir is None: 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) sys.exit(1)
project_name = project_dir project_name = project_dir
single_module_name = None single_module_name = None
if use_standard: if use_standard:
console.print()
console.print(f"[{UIColors.MUTED}]Common names: config, core, settings[/{UIColors.MUTED}]") console.print(f"[{UIColors.MUTED}]Common names: config, core, settings[/{UIColors.MUTED}]")
project_name = collector.get_validated_input( project_name = collector.get_validated_input(
"Enter Django project name", validate_project_name, "Django project name" "Enter Django project name", validate_project_name, "Django project name"
) )
elif use_single: elif use_single:
console.print()
console.print(f"[{UIColors.MUTED}]Common names: project, core, app[/{UIColors.MUTED}]") console.print(f"[{UIColors.MUTED}]Common names: project, core, app[/{UIColors.MUTED}]")
single_module_name = ( single_module_name = (
collector.get_validated_input( collector.get_validated_input(
@@ -495,9 +247,6 @@ def get_user_input() -> Tuple[str, str, str, list, dict]:
) )
# Step 2: Database Configuration # 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() database_type = collector.get_database_type_choice()
use_database_url = collector.get_database_config_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 = [] app_names = []
if use_standard: 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() nested, nested_dir = collector.get_nested_apps_config()
app_names = collector.get_app_names() app_names = collector.get_app_names()
# Step 3/4: CI/CD Pipeline # 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() use_github, use_gitlab = collector.get_cicd_choice()
# Generate Metadata # 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() return project_dir, project_name, app_names[0], app_names, metadata.to_dict()
else: else:
project_name, primary_app, app_names, metadata_dict = collector._get_structure_metadata( options = StructureOptions(
project_dir, project_dir=project_dir,
predefined=use_predefined, predefined=use_predefined,
unified=use_unified, unified=use_unified,
single=use_single, single=use_single,
@@ -543,6 +287,7 @@ def get_user_input() -> Tuple[str, str, str, list, dict]:
use_github=use_github, use_github=use_github,
use_gitlab=use_gitlab, 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 return project_dir, project_name, primary_app, app_names, metadata_dict
except KeyboardInterrupt: except KeyboardInterrupt:
@@ -576,8 +321,7 @@ def confirm_setup(project_dir: str, project_name: str, app_names: list, metadata
console.print() console.print()
try: try:
response = CharReader.get_yes_no(f"[{UIColors.WARNING}]Proceed with setup? (y/N):[/{UIColors.WARNING}]") return UIFormatter.confirm("Proceed with setup?", default=True)
return response == "y"
except KeyboardInterrupt: except KeyboardInterrupt:
UIFormatter.print_info("\nSetup cancelled by user.") 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): if metadata.get("use_gitlab_ci", False):
choices.append("GitLab CI") choices.append("GitLab CI")
return ", ".join(choices) if choices else "None" 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 pass
return False, None, search_dir 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 import string
from rich.console import Console from rich.console import Console
from rich.panel import Panel
from rich.table import Table from rich.table import Table
from rich.text import Text
from djinit.ui.console import UIColors
console = Console() console = Console()
@@ -38,22 +34,6 @@ def display_secret_keys(keys: list[str]) -> None:
console.print() console.print()
def generate_secret_command(): if __name__ == "__main__":
keys = generate_multiple_keys(3, 50) keys = generate_multiple_keys(3, 50)
display_secret_keys(keys) 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] [project]
name = "djinitx" name = "djinitx"
version = "0.3.0" version = "0.3.2"
description = "A CLI tool to setup Django project" description = "A CLI tool to setup Django project"
readme = "README.md" readme = "README.md"
requires-python = ">=3.13" requires-python = ">=3.10"
authors = [ authors = [
{ name = "Sankalp Tharu", email = "sankalptharu50028@gmail.com" } { name = "Sankalp Tharu", email = "sankalptharu50028@gmail.com" }
] ]
@@ -47,11 +47,9 @@ classifiers=[
"Topic :: Software Development :: Code Generators", "Topic :: Software Development :: Code Generators",
] ]
dependencies = [ dependencies = [
"click>=8.3.0",
"rich>=14.2.0", "rich>=14.2.0",
"ruff>=0.14.2",
"jinja2>=3.1.0", "jinja2>=3.1.0",
"pre-commit>=4.5.0", "questionary>=2.0.1",
] ]
[project.urls] [project.urls]
@@ -60,3 +58,9 @@ Homepage = "https://github.com/S4NKALP/djinit"
[project.scripts] [project.scripts]
djinit = "djinit.generator:main" djinit = "djinit.generator:main"
dj = "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 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( setup(
name="djinitx", name="djinitx",
version="0.3.0", version="0.3.2",
description="A CLI tool to setup Django projects", description="A CLI tool to setup Django projects",
long_description=open("README.md").read(), long_description=open("README.md").read(),
long_description_content_type="text/markdown", long_description_content_type="text/markdown",
@@ -16,6 +16,7 @@ setup(
"click>=8.3.0", "click>=8.3.0",
"rich>=14.2.0", "rich>=14.2.0",
"jinja2>=3.1.0", "jinja2>=3.1.0",
"questionary>=2.0.1",
], ],
entry_points={ entry_points={
"console_scripts": [ "console_scripts": [
Generated
+118 -27
View File
@@ -1,6 +1,6 @@
version = 1 version = 1
revision = 3 revision = 3
requires-python = ">=3.13" requires-python = ">=3.10"
[[package]] [[package]]
name = "cfgv" 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" }, { 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]] [[package]]
name = "distlib" name = "distlib"
version = "0.4.0" version = "0.4.0"
@@ -43,22 +22,30 @@ wheels = [
[[package]] [[package]]
name = "djinitx" name = "djinitx"
version = "0.3.0" version = "0.3.1"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "click" },
{ name = "jinja2" }, { name = "jinja2" },
{ name = "pre-commit" }, { name = "questionary" },
{ name = "rich" }, { name = "rich" },
]
[package.dev-dependencies]
dev = [
{ name = "pre-commit" },
{ name = "ruff" }, { name = "ruff" },
] ]
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "click", specifier = ">=8.3.0" },
{ name = "jinja2", specifier = ">=3.1.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" }, { name = "rich", specifier = ">=14.2.0" },
]
[package.metadata.requires-dev]
dev = [
{ name = "pre-commit", specifier = ">=4.5.0" },
{ name = "ruff", specifier = ">=0.14.2" }, { name = "ruff", specifier = ">=0.14.2" },
] ]
@@ -110,6 +97,39 @@ version = "3.0.3"
source = { registry = "https://pypi.org/simple" } 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" } 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 = [ 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/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/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" }, { 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" }, { 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]] [[package]]
name = "pygments" name = "pygments"
version = "2.19.2" version = "2.19.2"
@@ -214,6 +246,34 @@ version = "6.0.3"
source = { registry = "https://pypi.org/simple" } 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" } 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 = [ 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/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/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, { url = "https://files.pythonhosted.org/packages/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" }, { 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]] [[package]]
name = "rich" name = "rich"
version = "14.2.0" 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" }, { 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]] [[package]]
name = "virtualenv" name = "virtualenv"
version = "20.35.4" version = "20.35.4"
@@ -291,8 +372,18 @@ dependencies = [
{ name = "distlib" }, { name = "distlib" },
{ name = "filelock" }, { name = "filelock" },
{ name = "platformdirs" }, { 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" } 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 = [ 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" }, { 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" },
]