17 Commits
Author SHA1 Message Date
sankalp bb0def27f3 chore(release): bump version to 0.3.9 2026-10-26 19:54:03 +05:45
sankalp 95d61b8763 fix(deps): drop Python 3.10 to patch filelock
filelock 4.2.1+ fixes GHSA-v4x8-3wr5-3wq8 and related advisories, but requires Python >=3.11. Raise the minimum supported Python so the lockfile no longer resolves vulnerable filelock 4.1.0 for Python 3.10.
2026-10-26 19:45:26 +05:45
sankalp fefb47c1c3 chore(deps): bump packages to latest version 2026-10-26 19:39:11 +05:45
dependabot[bot] 4c766af6ab chore(deps): bump virtualenv from 21.7.13 to 21.14.13 (#76)
Bumps [virtualenv](https://github.com/pypa/virtualenv) from 21.7.13 to 21.14.13.
- [Release notes](https://github.com/pypa/virtualenv/releases)
- [Changelog](https://github.com/pypa/virtualenv/blob/main/docs/changelog.rst)
- [Commits](https://github.com/pypa/virtualenv/compare/21.7.13...21.14.13)

---
updated-dependencies:
- dependency-name: virtualenv
  dependency-version: 21.14.13
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-26 13:46:44 +00:00
dependabot[bot] c178c51696 chore(deps): bump ruff from 0.17.1 to 0.17.2 (#75)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.17.1 to 0.17.2.
- [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.17.1...0.17.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-26 02:24:58 +00:00
dependabot[bot] 5a9ea6c030 chore(deps): bump ruff from 0.16.10 to 0.17.1 (#74)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.16.10 to 0.17.1.
- [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.16.10...0.17.1)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.17.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-19 02:24:38 +00:00
dependabot[bot] 30411659e6 chore(deps): bump virtualenv from 20.36.1 to 21.7.13 (#73)
Bumps [virtualenv](https://github.com/pypa/virtualenv) from 20.36.1 to 21.7.13.
- [Release notes](https://github.com/pypa/virtualenv/releases)
- [Changelog](https://github.com/pypa/virtualenv/blob/main/docs/changelog.rst)
- [Commits](https://github.com/pypa/virtualenv/compare/20.36.1...21.7.13)

---
updated-dependencies:
- dependency-name: virtualenv
  dependency-version: 21.7.13
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-05 14:57:11 +00:00
dependabot[bot] 9f041a9938 chore(deps): bump ruff from 0.16.9 to 0.16.10 (#72)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.16.9 to 0.16.10.
- [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.16.9...0.16.10)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-05 02:24:38 +00:00
sankalp 8a3950c2cf chore(root): bump version to 0.3.8 2026-09-28 14:01:01 +05:45
sankalp 482ff13dd3 fix: replace eval() with an AST parser for template expressions
Empty __builtins__ is not a sandbox, so a template expression could
reach os.system. Walk the AST with an allow-list instead. Also fixes
app names missing from api/v1/urls.py and a wrong settings path in the
generated pyproject.toml.
2026-09-28 14:00:43 +05:45
dependabot[bot] 7c3ec11070 chore(deps): bump ruff from 0.16.8 to 0.16.9 (#70)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.16.8 to 0.16.9.
- [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.16.8...0.16.9)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-28 02:24:24 +00:00
dependabot[bot] aad053c52c chore(deps): bump ruff from 0.16.7 to 0.16.8 (#69)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.16.7 to 0.16.8.
- [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.16.7...0.16.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-21 02:24:36 +00:00
sankalp 298945835b style: fix trailing whitespace in input.py 2026-09-15 08:30:13 +05:45
sankalp 8619c220d3 feat: inject base.py accurately into specific project structures 2026-09-15 08:29:41 +05:45
sankalp 018d71e999 feat: add TimeStampedModel base.py-tpl to project initialization 2026-09-15 08:24:40 +05:45
sankalp 75c049d7ed fix: resolve '.' project name and clean up templates
- Resolve current directory to folder name when using '.'

- Remove obsolete setup.py

- Update Django classifier and template dependencies to 6.0

- Remove deprecated USE_TZ, coreapi schema, and drf-yasg
2026-09-15 08:19:12 +05:45
dependabot[bot] dd3ea6ca8e chore(deps): bump ruff from 0.16.6 to 0.16.7 (#68)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.16.6 to 0.16.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.16.6...0.16.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-14 02:26:51 +00:00
15 changed files with 612 additions and 214 deletions

No files matched your search

+3 -3
View File
@@ -7,10 +7,10 @@ packages = ["src/djinit"]
[project]
name = "djinitx"
version = "0.3.7"
version = "0.3.9"
description = "A CLI tool to setup Django project"
readme = "README.md"
requires-python = ">=3.10"
requires-python = ">=3.11"
authors = [
{ name = "Sankalp Tharu", email = "sankalptharu50028@gmail.com" }
]
@@ -29,7 +29,6 @@ keywords = [
classifiers=[
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
@@ -38,6 +37,7 @@ classifiers=[
"Framework :: Django :: 4.2",
"Framework :: Django :: 5.1",
"Framework :: Django :: 5.2",
"Framework :: Django :: 6.0",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
+3 -5
View File
@@ -1,5 +1,3 @@
questionary==2.1.1
rich==15.0.0
# dev
ruff==0.16.6
pre-commit==4.6.2
rich>=14.2.0
questionary>=2.0.1
typer>=0.21.1
-63
View File
@@ -1,63 +0,0 @@
from setuptools import find_packages, setup
setup(
name="djinitx",
version="0.3.7",
description="A CLI tool to setup Django projects",
long_description=open("README.md").read(),
long_description_content_type="text/markdown",
author="Sankalp Tharu",
author_email="sankalptharu50028@gmail.com",
url="https://github.com/S4NKALP/djinit",
packages=find_packages(where="src"),
package_dir={"": "src"},
include_package_data=True,
install_requires=[
"rich>=14.2.0",
"questionary>=2.0.1",
"typer>=0.21.1",
],
entry_points={
"console_scripts": [
"djinit=djinit.cli:app",
"dj=djinit.cli:app",
]
},
classifiers=[
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Environment :: Web Environment",
"Framework :: Django",
"Framework :: Django :: 4.2",
"Framework :: Django :: 5.1",
"Framework :: Django :: 5.2",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Topic :: Internet :: WWW/HTTP",
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Software Development :: Code Generators",
],
keywords=[
"django",
"cli",
"project",
"djinit",
"djinitx",
"dj",
"django-init",
"django project",
"django starter",
],
python_requires=">=3.10",
project_urls={
"Bug Reports": "https://github.com/S4NKALP/djinit/issues",
"Source": "https://github.com/S4NKALP/djinit",
"Documentation": "https://github.com/S4NKALP/djinit#readme",
},
)
+392 -17
View File
@@ -1,5 +1,375 @@
"""
Safe template expression evaluation for djinit.
Templates use a tiny, comment-style logic language:
# @IF <expression> ... # @ELSEIF <expression> ... # @ELSE ... # @ENDIF
# @LOOP <var> in <expression> ... # @ENDLOOP
[[ variable ]] variable / attribute / index substitution
Expressions are evaluated by walking the parsed syntax tree with an explicit
allow-list. ``eval()`` is never used: restricting ``__builtins__`` is *not* a
sandbox, because object graphs reachable from ordinary literals (for example
``().__class__.__base__.__subclasses__()``) still hand out ``os.system`` and
friends. An explicit interpreter has no such escape hatch.
"""
import ast
import operator
import re
from typing import Any, Dict, List
from collections.abc import Mapping
from typing import Any, Dict, List, Optional
__all__ = ["InFileLogicParser", "SafeExpressionError"]
class SafeExpressionError(ValueError):
"""Raised when a template expression is malformed or uses a disallowed feature."""
# The only names an expression may reference besides the render context. These are
# side-effect free helpers; nothing that can import, open, execute or mutate.
_SAFE_BUILTINS: Mapping[str, Any] = {
"True": True,
"False": False,
"None": None,
"abs": abs,
"all": all,
"any": any,
"bool": bool,
"dict": dict,
"enumerate": enumerate,
"float": float,
"int": int,
"len": len,
"list": list,
"max": max,
"min": min,
"reversed": reversed,
"sorted": sorted,
"str": str,
"zip": zip,
}
# Attribute names that must never be reachable, even on an otherwise harmless
# object. Together with rejecting every ``_``-prefixed name this closes the usual
# attribute-hop escape routes (``__class__``, ``__globals__``, ``__subclasses__``…).
# ``format``/``format_map`` are included because the *format string* they receive is
# resolved with ``getattr`` inside CPython, which would sidestep the checks below.
_FORBIDDEN_ATTRIBUTES = frozenset(
{
"__import__",
"breakpoint",
"compile",
"delattr",
"eval",
"exec",
"exit",
"format",
"format_map",
"fork",
"getattr",
"globals",
"input",
"locals",
"open",
"popen",
"quit",
"setattr",
"spawn",
"system",
"vars",
}
)
# Calling a method is only allowed on plain data values. Without this, a context
# value that happens to be a module, class or function would expose its whole
# (dangerous) surface through dotted access.
_PLAIN_DATA_TYPES = (str, bytes, bytearray, bool, int, float, complex, list, tuple, dict, set, frozenset, Mapping)
# Guards against cheap resource exhaustion such as ``2 ** 99999999``.
_MAX_EXPONENT = 1000
class _SafeExpressionEvaluator(ast.NodeVisitor):
"""Interprets a validated expression tree against the render context."""
_BINARY_OPERATORS = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
ast.FloorDiv: operator.floordiv,
ast.Mod: operator.mod,
ast.Pow: operator.pow,
}
_UNARY_OPERATORS = {
ast.UAdd: operator.pos,
ast.USub: operator.neg,
ast.Not: operator.not_,
ast.Invert: operator.invert,
}
_COMPARISON_OPERATORS = {
ast.Eq: operator.eq,
ast.NotEq: operator.ne,
ast.Lt: operator.lt,
ast.LtE: operator.le,
ast.Gt: operator.gt,
ast.GtE: operator.ge,
ast.Is: operator.is_,
ast.IsNot: operator.is_not,
ast.In: lambda a, b: operator.contains(b, a),
ast.NotIn: lambda a, b: not operator.contains(b, a),
}
def __init__(self, context: Mapping[str, Any]) -> None:
# Outermost scope is the render context; comprehension bodies push their own
# dicts so loop variables never leak into the surrounding scope.
self._scopes: List[Any] = [context]
# -- entry point ---------------------------------------------------------
def evaluate(self, tree: ast.Expression) -> Any:
return self.visit(tree.body)
def generic_visit(self, node: ast.AST) -> Any:
raise SafeExpressionError(f"{type(node).__name__} is not allowed in template expressions")
# -- literals ------------------------------------------------------------
def visit_Constant(self, node: ast.Constant) -> Any:
return node.value
def visit_List(self, node: ast.List) -> list:
return [self.visit(element) for element in node.elts]
def visit_Tuple(self, node: ast.Tuple) -> tuple:
return tuple(self.visit(element) for element in node.elts)
def visit_Set(self, node: ast.Set) -> set:
return {self.visit(element) for element in node.elts}
def visit_Dict(self, node: ast.Dict) -> dict:
result: dict = {}
for key_node, value_node in zip(node.keys, node.values):
if key_node is None:
raise SafeExpressionError("Dictionary unpacking is not allowed")
result[self.visit(key_node)] = self.visit(value_node)
return result
def visit_JoinedStr(self, node: ast.JoinedStr) -> str:
return "".join(str(self.visit(value)) for value in node.values)
def visit_FormattedValue(self, node: ast.FormattedValue) -> str:
value = self.visit(node.value)
if node.conversion == 115: # !s
return f"{value:s}"
if node.conversion == 114: # !r
return repr(value)
if node.conversion == 97: # !a
return ascii(value)
if node.format_spec is not None:
return format(value, self.visit(node.format_spec))
return str(value)
# -- names and attributes ------------------------------------------------
def visit_Name(self, node: ast.Name) -> Any:
return self._lookup(node.id)
def _lookup(self, name: str) -> Any:
for scope in reversed(self._scopes):
if name in scope:
return scope[name]
if name in _SAFE_BUILTINS:
return _SAFE_BUILTINS[name]
raise SafeExpressionError(f"Unknown name: {name!r}")
def visit_Attribute(self, node: ast.Attribute) -> Any:
return self._resolve_attribute(self.visit(node.value), node.attr)
@staticmethod
def _resolve_attribute(value: Any, name: str) -> Any:
if name.startswith("_") or name in _FORBIDDEN_ATTRIBUTES:
raise SafeExpressionError(f"Access to attribute {name!r} is not allowed")
if isinstance(value, Mapping):
# Templates routinely index plain dicts with dotted syntax.
try:
return value[name]
except KeyError:
raise AttributeError(name) from None
resolved = getattr(value, name)
if callable(resolved) and not isinstance(value, _PLAIN_DATA_TYPES):
raise SafeExpressionError(f"Access to method {name!r} is not allowed here")
return resolved
# -- operators -----------------------------------------------------------
def visit_BoolOp(self, node: ast.BoolOp) -> Any:
if isinstance(node.op, ast.And):
result: Any = True
for value in node.values:
result = self.visit(value)
if not result:
return result
return result
result = False
for value in node.values:
result = self.visit(value)
if result:
return result
return result
def visit_UnaryOp(self, node: ast.UnaryOp) -> Any:
handler = self._UNARY_OPERATORS.get(type(node.op))
if handler is None:
raise SafeExpressionError(f"Unary operator {type(node.op).__name__} is not allowed")
return handler(self.visit(node.operand))
def visit_BinOp(self, node: ast.BinOp) -> Any:
handler = self._BINARY_OPERATORS.get(type(node.op))
if handler is None:
raise SafeExpressionError(f"Binary operator {type(node.op).__name__} is not allowed")
left = self.visit(node.left)
right = self.visit(node.right)
if isinstance(node.op, ast.Pow) and isinstance(left, int) and isinstance(right, int):
if abs(right) > _MAX_EXPONENT:
raise SafeExpressionError("Exponent is too large")
return handler(left, right)
def visit_Compare(self, node: ast.Compare) -> bool:
left = self.visit(node.left)
for operator_node, comparator_node in zip(node.ops, node.comparators):
handler = self._COMPARISON_OPERATORS.get(type(operator_node))
if handler is None:
raise SafeExpressionError(f"Comparison {type(operator_node).__name__} is not allowed")
right = self.visit(comparator_node)
if not handler(left, right):
return False
left = right
return True
def visit_IfExp(self, node: ast.IfExp) -> Any:
return self.visit(node.body) if self.visit(node.test) else self.visit(node.orelse)
# -- subscripts ----------------------------------------------------------
def visit_Subscript(self, node: ast.Subscript) -> Any:
value = self.visit(node.value)
key = self._resolve_subscript_key(node.slice, value)
try:
return value[key]
except TypeError as exc:
raise SafeExpressionError(f"Cannot subscript {type(value).__name__}") from exc
def _resolve_subscript_key(self, node: ast.AST, value: Any) -> Any:
if isinstance(node, ast.Slice):
return slice(
self._resolve_subscript_bound(node.lower),
self._resolve_subscript_bound(node.upper),
self._resolve_subscript_bound(node.step),
)
if isinstance(node, ast.Tuple):
return tuple(self.visit(element) for element in node.elts)
return self.visit(node)
def _resolve_subscript_bound(self, node: Optional[ast.AST]) -> Optional[Any]:
return None if node is None else self.visit(node)
# -- calls ---------------------------------------------------------------
def visit_Call(self, node: ast.Call) -> Any:
if any(keyword.arg is None for keyword in node.keywords):
raise SafeExpressionError("** unpacking is not allowed in calls")
args = [self.visit(argument) for argument in node.args]
kwargs = {keyword.arg: self.visit(keyword.value) for keyword in node.keywords}
callee = self.visit(node.func)
if not callable(callee):
raise SafeExpressionError(f"{callee!r} is not callable")
return callee(*args, **kwargs)
# -- comprehensions ------------------------------------------------------
def visit_ListComp(self, node: ast.ListComp) -> list:
result: list = []
self._run_comprehension(node.generators, 0, lambda: result.append(self.visit(node.elt)))
return result
def visit_SetComp(self, node: ast.SetComp) -> set:
result: set = set()
self._run_comprehension(node.generators, 0, lambda: result.add(self.visit(node.elt)))
return result
def visit_GeneratorExp(self, node: ast.GeneratorExp) -> tuple:
result: list = []
self._run_comprehension(node.generators, 0, lambda: result.append(self.visit(node.elt)))
return tuple(result)
def visit_DictComp(self, node: ast.DictComp) -> dict:
result: dict = {}
self._run_comprehension(
node.generators,
0,
lambda: result.__setitem__(self.visit(node.key), self.visit(node.value)),
)
return result
def _run_comprehension(self, generators: List[ast.comprehension], index: int, emit) -> None:
if index >= len(generators):
emit()
return
generator = generators[index]
for value in self._as_iterable(self.visit(generator.iter)):
self._scopes.append({})
try:
self._assign(generator.target, value)
if all(self.visit(condition) for condition in generator.ifs):
self._run_comprehension(generators, index + 1, emit)
finally:
self._scopes.pop()
@staticmethod
def _as_iterable(value: Any) -> Any:
if isinstance(value, (str, bytes)) or not hasattr(value, "__iter__"):
raise SafeExpressionError(f"{type(value).__name__} is not iterable")
return value
def _assign(self, target: ast.AST, value: Any) -> None:
if isinstance(target, ast.Name):
self._scopes[-1][target.id] = value
return
if isinstance(target, (ast.Tuple, ast.List)) and isinstance(value, (tuple, list)):
if len(target.elts) != len(value):
raise SafeExpressionError("Cannot unpack mismatched number of loop variables")
for element, item in zip(target.elts, value):
self._assign(element, item)
return
raise SafeExpressionError("Unsupported loop target")
def safe_eval(expression: str, context: Optional[Mapping[str, Any]] = None) -> Any:
"""Evaluate a template expression without ``eval()``.
Args:
expression: The expression source, e.g. ``"use_tailwind and len(app_names) > 0"``.
context: Names available to the expression.
Raises:
SafeExpressionError: If the expression is empty, malformed or disallowed.
"""
source = expression.strip()
if not source:
raise SafeExpressionError("Empty expression")
try:
tree = ast.parse(source, mode="eval")
except SyntaxError as exc:
raise SafeExpressionError(f"Invalid expression syntax: {source!r}") from exc
return _SafeExpressionEvaluator(context or {}).evaluate(tree)
class InFileLogicParser:
@@ -11,18 +381,21 @@ class InFileLogicParser:
def __init__(self, context: Dict[str, Any] = None):
self.context = context or {}
def _evaluate(self, expression: str) -> Any:
"""Evaluate ``expression`` against the current context, safely."""
return safe_eval(expression, self.context)
def _get_value(self, key: str) -> str:
"""
Supports nested/dotted access like [[ user.name ]] or [[ settings["DEBUG"] ]].
For simplicity, it uses eval in the provided context for complex expressions,
but falls back to safe dictionary lookup for simple keys.
Dotted names are resolved against the context, and arbitrary expressions are
evaluated by the safe interpreter. Anything that cannot be resolved is
returned verbatim so a missing context key stays visible in the output.
"""
key = key.strip()
try:
# Try evaluating the expression in the context
return str(eval(key, {"__builtins__": {}}, self.context))
except (NameError, SyntaxError, KeyError, TypeError, AttributeError):
# Fallback for common patterns or just return the key as is if it fails
return str(self._evaluate(key))
except (AttributeError, IndexError, KeyError, TypeError, ValueError):
return f"[[ {key} ]]"
def render(self, template_text: str, context: Dict[str, Any] = None) -> str:
@@ -44,8 +417,8 @@ class InFileLogicParser:
if stripped.startswith("# @IF "):
expr = stripped[6:].strip()
try:
result = bool(eval(expr, {"__builtins__": {}}, self.context))
except Exception:
result = bool(self._evaluate(expr))
except (AttributeError, IndexError, KeyError, TypeError, ValueError):
result = False
stack.append([result, result])
i += 1
@@ -65,8 +438,8 @@ class InFileLogicParser:
current_stack[0] = False
else:
try:
result = bool(eval(expr, {"__builtins__": {}}, self.context))
except Exception:
result = bool(self._evaluate(expr))
except (AttributeError, IndexError, KeyError, TypeError, ValueError):
result = False
current_stack[0] = result
if result:
@@ -113,13 +486,15 @@ class InFileLogicParser:
i += 1
continue
parts = stripped[7:].strip().split(" in ")
if len(parts) == 2:
parts = stripped[7:].strip().split(" in ", 1)
if len(parts) == 2 and parts[0].strip() and parts[1].strip():
var_name = parts[0].strip()
iterable_name = parts[1].strip()
try:
iterable = eval(iterable_name, {"__builtins__": {}}, self.context)
except Exception:
iterable = self._evaluate(iterable_name)
if not hasattr(iterable, "__iter__"):
raise TypeError(f"{iterable_name!r} is not iterable")
except (AttributeError, IndexError, KeyError, TypeError, ValueError):
iterable = []
# Capture loop body
@@ -197,14 +572,14 @@ class InFileLogicParser:
post_content = expr_parts[1].lstrip()
try:
if bool(eval(expr, {"__builtins__": {}}, self.context)):
if bool(self._evaluate(expr)):
# If Case 1, we want pre_content. If Case 2, we want post_content.
# Usually, if pre_content is empty (or just whitespace), it's Case 2.
if pre_content:
final_lines.append(pre_content + post_content)
else:
final_lines.append(post_content)
except Exception:
except (AttributeError, IndexError, KeyError, TypeError, ValueError):
pass
i += 1
continue
+56
View File
@@ -8,6 +8,7 @@ import os
from djinit.core.base import BaseService
from djinit.templater import template_engine
from djinit.utils.common import CommonUtils
from djinit.utils.django import DjangoHelper
class FileCreator(BaseService):
@@ -195,12 +196,26 @@ class FileCreator(BaseService):
"Created requirements.txt with Django dependencies",
)
def _registered_app_modules(self) -> list:
"""Dotted module paths of the apps that end up in INSTALLED_APPS.
The unified and single layouts use the generated package itself as their
only app, so they have no separate ``app_names`` to report.
"""
if self.metadata.get("unified_structure"):
return ["apps"]
if self.metadata.get("single_structure"):
return [self.module_name]
return CommonUtils.calculate_app_module_paths(self.app_names, self.metadata)
def create_readme(self) -> None:
context = {
"project_name": self.project_name,
"app_names": self.app_names,
"app_list": self._registered_app_modules(),
"predefined_structure": bool(self.metadata.get("predefined_structure")),
"unified_structure": bool(self.metadata.get("unified_structure")),
"single_structure": bool(self.metadata.get("single_structure")),
"module_name": self.module_name,
}
self._render_and_create_file("README.md", "project/readme.md-tpl", context, "Created README.md file")
@@ -285,6 +300,19 @@ class FileCreator(BaseService):
core_subfolders = ["utils", "mixins", "middleware"]
self._create_subdirectories_with_init(core_dir, core_subfolders, "apps/core/")
# Inject base model
CommonUtils.create_file_from_template(
os.path.join(core_dir, "base.py"),
"components/base.py-tpl",
{},
"Created apps/core/base.py",
)
# Every app that ends up in USER_DEFINED_APPS needs a real app module,
# otherwise the generated project cannot start.
for app_name in self.app_names or ["users", "core"]:
DjangoHelper.startapp(app_name, apps_dir, f"apps.{app_name}")
# Create api directory structure
api_dir = os.path.join(self.project_root, "api")
CommonUtils.create_directory_with_init(api_dir, "Created api/__init__.py")
@@ -352,6 +380,14 @@ class FileCreator(BaseService):
components = ["admin", "models", "serializers", "tests", "urls", "views"]
self._create_subdirectories_with_init(apps_dir, components, "apps/")
# Inject base model
CommonUtils.create_file_from_template(
os.path.join(apps_dir, "models", "base.py"),
"components/base.py-tpl",
{},
"Created apps/models/base.py",
)
# Create 'api' directory with v1
api_dir = os.path.join(apps_dir, "api")
CommonUtils.create_directory_with_init(api_dir, "Created apps/api/__init__.py")
@@ -401,6 +437,14 @@ class FileCreator(BaseService):
components = ["admin", "api", "models", "tests"]
self._create_subdirectories_with_init(project_dir, components, f"{self.module_name}/")
# Inject base model
CommonUtils.create_file_from_template(
os.path.join(project_dir, "models", "base.py"),
"components/base.py-tpl",
{},
f"Created {self.module_name}/models/base.py",
)
# Create README files for guidance
CommonUtils.create_file_from_template(
os.path.join(project_dir, "api", "README.md"),
@@ -510,3 +554,15 @@ class FileCreator(BaseService):
CommonUtils.create_file_with_content(
filepath, json.dumps(config, indent=4), "Created .djinit configuration file"
)
def create_standard_shared(self) -> None:
"""Create shared directory and inject base model for standard structure."""
shared_dir = os.path.join(self.project_root, "shared")
CommonUtils.create_directory_with_init(shared_dir, "Created shared/__init__.py")
CommonUtils.create_file_from_template(
os.path.join(shared_dir, "base.py"),
"components/base.py-tpl",
{},
"Created shared/base.py",
)
+4
View File
@@ -35,6 +35,7 @@ class SetupCreator:
if not self.app_names:
self.app_names = ["users", "core"]
self.project_creator.app_names = self.app_names
self.file_creator.app_names = self.app_names
elif self.metadata.get("unified_structure"):
self.metadata.setdefault("project_module_name", "core")
@@ -43,12 +44,14 @@ class SetupCreator:
if not self.app_names:
self.app_names = []
self.project_creator.app_names = self.app_names
self.file_creator.app_names = self.app_names
elif self.metadata.get("single_structure"):
self.metadata.setdefault("nested_apps", False)
if not self.app_names:
self.app_names = []
self.project_creator.app_names = self.app_names
self.file_creator.app_names = self.app_names
def create(self) -> bool:
"""Run the full setup process."""
@@ -68,6 +71,7 @@ class SetupCreator:
else:
steps.append(("Creating Django apps", self.project_creator.create_apps))
steps.append(("Creating project URLs", self.file_creator.create_project_urls))
steps.append(("Creating shared module", self.file_creator.create_standard_shared))
steps.extend(
[
@@ -0,0 +1,11 @@
"""generated with djinit"""
from django.db import models
class TimeStampedModel(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
@@ -17,7 +17,7 @@ environ.Env.read_env(env_path)
BASE_DIR = Path(__file__).resolve().parent.parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/6.1/howto/deployment/checklist/
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
# Application definition
@@ -26,7 +26,6 @@ THIRD_PARTY_APPS = [
"rest_framework_simplejwt",
"rest_framework_simplejwt.token_blacklist",
"corsheaders",
"drf_yasg",
"django_tailwind_cli", # @IF use_tailwind
"django_htmx", # @IF use_htmx
]
@@ -83,7 +82,7 @@ WSGI_APPLICATION = "[[ project_name ]].wsgi.application"
# Password validation
# https://docs.djangoproject.com/en/6.1/ref/settings/#auth-password-validators
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
@@ -102,12 +101,11 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization
# https://docs.djangoproject.com/en/6.1/topics/i18n/
# https://docs.djangoproject.com/en/6.0/topics/i18n/
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True
# Static files and media files
STATIC_URL = "/static/"
@@ -139,7 +137,6 @@ REST_FRAMEWORK = {
"rest_framework_simplejwt.authentication.JWTAuthentication",
"rest_framework.authentication.SessionAuthentication",
),
"DEFAULT_SCHEMA_CLASS": "rest_framework.schemas.coreapi.AutoSchema",
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 20,
}
@@ -10,7 +10,7 @@ DEBUG = True
ALLOWED_HOSTS = ["*"]
# Database
# https://docs.djangoproject.com/en/6.1/ref/settings/#databases
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
DATABASES = {
"default": {
-18
View File
@@ -9,8 +9,6 @@ from django.views.static import serve
from django.urls import include
# @ENDIF
# @IF include_api
from drf_yasg.views import get_schema_view
from drf_yasg import openapi
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
@@ -39,22 +37,6 @@ urlpatterns = [
# @ENDLOOP
# @ENDIF
]
# @IF include_api
# Don't show schema in production
if settings.DEBUG:
schema_view = get_schema_view(
openapi.Info(
title="[[ project_name ]] API",
default_version="v1",
),
public=True,
)
urlpatterns += [
path("docs/", schema_view.with_ui("swagger", cache_timeout=0)),
path("schema/", schema_view.with_ui("redoc", cache_timeout=0)),
]
# @ENDIF
# @IF use_tailwind
# django-browser-reload for tailwind live reload (dev only)
@@ -1,17 +1,16 @@
[project]
name = "[[ project_name ]]"
name = "[[ package_name ]]"
version = "0.1.0"
description = ""
readme = "README.md"
requires-python = ">=3.13"
license = { text = "MIT" }
dependencies = [
"django>=6.1,<7.0",
"django>=6.0,<7.0",
"django-environ",
"django-jazzmin",
"djangorestframework",
"djangorestframework-simplejwt",
"drf-yasg",
"django-cors-headers",
"whitenoise",
# @IF database_type == 'mysql'
@@ -62,7 +61,7 @@ ignore = [
]
[tool.ruff.lint.isort]
known-first-party = ["[[ project_name ]]"]
known-first-party = ["[[ module_name ]]"]
[tool.ruff.lint.per-file-ignores]
"[[ project_name ]]/settings/*.py" = ["F403", "F405"]
"[[ module_name ]]/settings/*.py" = ["F403", "F405"]
@@ -9,6 +9,16 @@ A Django project folder structure is generated with [djinitx/djinit/dj](https://
- Essential dependencies and utilities
- Production-ready configuration
# @IF app_list
## Apps
Registered in `[[ module_name ]]/settings/base.py`:
# @LOOP app in app_list
- `[[ app ]]`
# @ENDLOOP
# @ENDIF
## Setup
### Using Just (Recommended)
@@ -7,7 +7,6 @@ django-environ
django-jazzmin
djangorestframework
djangorestframework_simplejwt
drf-yasg
django-cors-headers
whitenoise
django-tailwind-cli # @IF use_tailwind
+3
View File
@@ -187,6 +187,9 @@ class InputCollector:
"""Helper method to generate metadata dictionary."""
project_dir = options["project_dir"]
project_name = project_dir
if project_name == "." or not project_name:
project_name = os.path.basename(os.path.abspath(os.getcwd()))
app_names: list[str] = []
# Default package_name to "backend" if project_dir is "." or empty
Generated
+122 -95
View File
@@ -1,6 +1,15 @@
version = 1
revision = 3
requires-python = ">=3.10"
revision = 5
requires-python = ">=3.11"
[[package]]
name = "annotated-doc"
version = "0.0.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" },
]
[[package]]
name = "cfgv"
@@ -11,18 +20,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.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
@@ -34,16 +31,16 @@ wheels = [
[[package]]
name = "distlib"
version = "0.4.0"
version = "0.4.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" }
sdist = { url = "https://files.pythonhosted.org/packages/68/bc/d36966de7304047a1001a7559fbf266c464bb5d8cc85464536ef8c9bcc32/distlib-0.4.4.tar.gz", hash = "sha256:ca4175b4ac7f71223cc30ed7248c45d2b38c2439d8ee6e498ab67ed63cf3508d", size = 621304, upload-time = "2026-10-16T13:56:34.203Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
{ url = "https://files.pythonhosted.org/packages/c1/2b/238f02a4927fa2e27aa995371d240a468b52e42eae44a678434aff736b38/distlib-0.4.4-py2.py3-none-any.whl", hash = "sha256:cf09d55b1e85a89576b036aeb71550bfd68320f3f21761ca329eadadad918638", size = 472099, upload-time = "2026-10-16T13:56:32.053Z" },
]
[[package]]
name = "djinitx"
version = "0.3.7"
version = "0.3.9"
source = { editable = "." }
dependencies = [
{ name = "questionary" },
@@ -72,32 +69,32 @@ dev = [
[[package]]
name = "filelock"
version = "3.20.3"
version = "4.2.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/28/3893324bcadf8f89b1616637468d766af3f625008c0735bf704fdfa86617/filelock-4.2.4.tar.gz", hash = "sha256:c6d8663535e1650ee430507b4ffbac7b464dd64b7647ea0112a9f406499d163d", size = 652444, upload-time = "2026-10-19T01:57:51.001Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" },
{ url = "https://files.pythonhosted.org/packages/e0/1b/5e74289a1878e666bb4b65fe5805a04cafcac2aa5f519ef1ead67ef91c56/filelock-4.2.4-py3-none-any.whl", hash = "sha256:301241438c57936ef93f4243a94f2559022268bfcf9b375a9a72eb0f8db8358e", size = 162475, upload-time = "2026-10-19T01:57:49.419Z" },
]
[[package]]
name = "identify"
version = "2.6.15"
version = "2.6.20"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" }
sdist = { url = "https://files.pythonhosted.org/packages/53/35/d70c0006c7cee65999ea94a6273e60b2094f600a3d8b71b04318253fc643/identify-2.6.20.tar.gz", hash = "sha256:ad729860a923858d26917c2f4fb0a1d83d27a75b1e090c06440c573f048f3285", size = 99681, upload-time = "2026-09-26T20:29:07.187Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" },
{ url = "https://files.pythonhosted.org/packages/fa/70/fffc9613501877c0a10a1ef73a165e6ded3e53f6e7502227fef56d973933/identify-2.6.20-py2.py3-none-any.whl", hash = "sha256:6a16b69b93187244e0548cbfd25b3e4a6f9a7a2ad784625c3bec2b8d27b81aaa", size = 99506, upload-time = "2026-09-26T20:29:06.054Z" },
]
[[package]]
name = "markdown-it-py"
version = "4.0.0"
version = "4.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mdurl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
{ url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
]
[[package]]
@@ -111,25 +108,34 @@ wheels = [
[[package]]
name = "nodeenv"
version = "1.9.1"
version = "1.11.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" }
sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/105de02c1322cfada6d9710d9146ef8026419d433c9d08359a2d35805811/nodeenv-1.11.0.tar.gz", hash = "sha256:3ce8fe5b71d16e8af7039ca65257354100bc772965d6bc549070649e53b1b146", size = 99301, upload-time = "2026-09-26T11:29:21.367Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" },
{ url = "https://files.pythonhosted.org/packages/54/c8/12811c9b48fde162bb72b6f2e78fada9a247a09d7bb5be2050a5d099c77b/nodeenv-1.11.0-py2.py3-none-any.whl", hash = "sha256:edaa16e6c14d7cf395d75d4bbd5a26390f4dc06501a33b4e76282b02cc688a25", size = 34277, upload-time = "2026-09-26T11:29:19.933Z" },
]
[[package]]
name = "packaging"
version = "26.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
]
[[package]]
name = "platformdirs"
version = "4.5.0"
version = "4.13.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" }
sdist = { url = "https://files.pythonhosted.org/packages/58/e6/d0abbb256b44e6a05748c6942511ff67db8dbb068de0c35b24bf1b395b09/platformdirs-4.13.5.tar.gz", hash = "sha256:05831adf87d392760f4b5c3a0a95579ab5a10b18b9b9bd43680dcb47544788a0", size = 62362, upload-time = "2026-10-19T01:02:11.778Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" },
{ url = "https://files.pythonhosted.org/packages/6c/8a/1d8cb3842909dc193a73835adedf412e32e18f8ffd59f43983949622fe6f/platformdirs-4.13.5-py3-none-any.whl", hash = "sha256:53d3ec4280b815c1e0b02edad7f9ee8fda1ba9c0865dd9c2e6e63849ce82fc1e", size = 32779, upload-time = "2026-10-19T01:02:10.17Z" },
]
[[package]]
name = "pre-commit"
version = "4.5.0"
version = "4.7.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cfgv" },
@@ -138,30 +144,42 @@ dependencies = [
{ name = "pyyaml" },
{ name = "virtualenv" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f4/9b/6a4ffb4ed980519da959e1cf3122fc6cb41211daa58dbae1c73c0e519a37/pre_commit-4.5.0.tar.gz", hash = "sha256:dc5a065e932b19fc1d4c653c6939068fe54325af8e741e74e88db4d28a4dd66b", size = 198428, upload-time = "2025-11-22T21:02:42.304Z" }
sdist = { url = "https://files.pythonhosted.org/packages/3a/c3/6965c2690ddce885ec75121572d065a5b94c5a5d8d3e657927ecda9fd715/pre_commit-4.7.0.tar.gz", hash = "sha256:1f3deee12b914c8fa184bd2b5953391c2e4f36763e403d4a23dc9983fa7c6e86", size = 198120, upload-time = "2026-10-12T20:45:40.32Z" }
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/eb/83/ce0a196e4f7b20b11ce3aa1fc72fb973f64b7628d51c1b8aebad37fb7f81/pre_commit-4.7.0-py2.py3-none-any.whl", hash = "sha256:2e229038ad3656081b70c27913157e4b636ef306f643f024a8a8e3f0f7b2f877", size = 226261, upload-time = "2026-10-12T20:45:38.858Z" },
]
[[package]]
name = "prompt-toolkit"
version = "3.0.52"
version = "3.0.53"
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" }
sdist = { url = "https://files.pythonhosted.org/packages/7d/ea/39b988c938f75cb75d7045b5c69f8bfed47ee2152c8837fb403de29d6fb8/prompt_toolkit-3.0.53.tar.gz", hash = "sha256:9ec8a0ad96d5c56148b3f914aa79c1564c3fde5d2e6b876e7bc327e353cf8fa6", size = 435492, upload-time = "2026-07-26T20:56:14.758Z" }
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" },
{ url = "https://files.pythonhosted.org/packages/54/6f/84908cad2d6aa5144abcf7b42709fe4fdb459bc640ec7ac5786e7693dabc/prompt_toolkit-3.0.53-py3-none-any.whl", hash = "sha256:01c0891d7f9237d5e339f7d3e42cdae80b7534abb1c7c0e3352efba6231492f2", size = 392288, upload-time = "2026-07-26T20:56:12.512Z" },
]
[[package]]
name = "pygments"
version = "2.20.0"
version = "2.21.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
{ url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
]
[[package]]
name = "python-discovery"
version = "1.6.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "filelock" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f3/ee/cf608372c88ce2c136ee13c69ab6f667dbc0d0c1176af188b4ddc8853e66/python_discovery-1.6.4.tar.gz", hash = "sha256:bc70d31f0ec5de11eda13d4d64c16e24b3506a65984684f2b6c98cb83b2ea9df", size = 89883, upload-time = "2026-10-19T01:45:14.855Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/60/91/1aa70d3dfcab0dbcf88a2cbc3bce7f7c69562287a90216e617eaa4937e25/python_discovery-1.6.4-py3-none-any.whl", hash = "sha256:530179cc51a1816d512b15635b3dc931295ae12efb183783ad31190a5d425d2b", size = 39385, upload-time = "2026-10-19T01:45:13.668Z" },
]
[[package]]
@@ -170,15 +188,6 @@ 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" },
@@ -242,41 +251,40 @@ wheels = [
[[package]]
name = "rich"
version = "14.2.0"
version = "15.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown-it-py" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" },
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
]
[[package]]
name = "ruff"
version = "0.14.2"
version = "0.17.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/34/8218a19b2055b80601e8fd201ec723c74c7fe1ca06d525a43ed07b6d8e85/ruff-0.14.2.tar.gz", hash = "sha256:98da787668f239313d9c902ca7c523fe11b8ec3f39345553a51b25abc4629c96", size = 5539663, upload-time = "2025-10-23T19:37:00.956Z" }
sdist = { url = "https://files.pythonhosted.org/packages/ae/58/690cf56da80c8f2c8bf12042b6017beac2c34fe961f65420bcb3fb7fac38/ruff-0.17.2.tar.gz", hash = "sha256:84d632402d14c23b488f941db23712d6736a013225162062d34558c3f3c780eb", size = 5333400, upload-time = "2026-10-22T18:38:00.181Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/16/dd/23eb2db5ad9acae7c845700493b72d3ae214dce0b226f27df89216110f2b/ruff-0.14.2-py3-none-linux_armv6l.whl", hash = "sha256:7cbe4e593505bdec5884c2d0a4d791a90301bc23e49a6b1eb642dd85ef9c64f1", size = 12533390, upload-time = "2025-10-23T19:36:18.044Z" },
{ url = "https://files.pythonhosted.org/packages/5a/8c/5f9acff43ddcf3f85130d0146d0477e28ccecc495f9f684f8f7119b74c0d/ruff-0.14.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8d54b561729cee92f8d89c316ad7a3f9705533f5903b042399b6ae0ddfc62e11", size = 12887187, upload-time = "2025-10-23T19:36:22.664Z" },
{ url = "https://files.pythonhosted.org/packages/99/fa/047646491479074029665022e9f3dc6f0515797f40a4b6014ea8474c539d/ruff-0.14.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5c8753dfa44ebb2cde10ce5b4d2ef55a41fb9d9b16732a2c5df64620dbda44a3", size = 11925177, upload-time = "2025-10-23T19:36:24.778Z" },
{ url = "https://files.pythonhosted.org/packages/15/8b/c44cf7fe6e59ab24a9d939493a11030b503bdc2a16622cede8b7b1df0114/ruff-0.14.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d0bbeffb8d9f4fccf7b5198d566d0bad99a9cb622f1fc3467af96cb8773c9e3", size = 12358285, upload-time = "2025-10-23T19:36:26.979Z" },
{ url = "https://files.pythonhosted.org/packages/45/01/47701b26254267ef40369aea3acb62a7b23e921c27372d127e0f3af48092/ruff-0.14.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7047f0c5a713a401e43a88d36843d9c83a19c584e63d664474675620aaa634a8", size = 12303832, upload-time = "2025-10-23T19:36:29.192Z" },
{ url = "https://files.pythonhosted.org/packages/2d/5c/ae7244ca4fbdf2bee9d6405dcd5bc6ae51ee1df66eb7a9884b77b8af856d/ruff-0.14.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bf8d2f9aa1602599217d82e8e0af7fd33e5878c4d98f37906b7c93f46f9a839", size = 13036995, upload-time = "2025-10-23T19:36:31.861Z" },
{ url = "https://files.pythonhosted.org/packages/27/4c/0860a79ce6fd4c709ac01173f76f929d53f59748d0dcdd662519835dae43/ruff-0.14.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1c505b389e19c57a317cf4b42db824e2fca96ffb3d86766c1c9f8b96d32048a7", size = 14512649, upload-time = "2025-10-23T19:36:33.915Z" },
{ url = "https://files.pythonhosted.org/packages/7f/7f/d365de998069720a3abfc250ddd876fc4b81a403a766c74ff9bde15b5378/ruff-0.14.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a307fc45ebd887b3f26b36d9326bb70bf69b01561950cdcc6c0bdf7bb8e0f7cc", size = 14088182, upload-time = "2025-10-23T19:36:36.983Z" },
{ url = "https://files.pythonhosted.org/packages/6c/ea/d8e3e6b209162000a7be1faa41b0a0c16a133010311edc3329753cc6596a/ruff-0.14.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:61ae91a32c853172f832c2f40bd05fd69f491db7289fb85a9b941ebdd549781a", size = 13599516, upload-time = "2025-10-23T19:36:39.208Z" },
{ url = "https://files.pythonhosted.org/packages/fa/ea/c7810322086db68989fb20a8d5221dd3b79e49e396b01badca07b433ab45/ruff-0.14.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1967e40286f63ee23c615e8e7e98098dedc7301568bd88991f6e544d8ae096", size = 13272690, upload-time = "2025-10-23T19:36:41.453Z" },
{ url = "https://files.pythonhosted.org/packages/a9/39/10b05acf8c45786ef501d454e00937e1b97964f846bf28883d1f9619928a/ruff-0.14.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2877f02119cdebf52a632d743a2e302dea422bfae152ebe2f193d3285a3a65df", size = 13496497, upload-time = "2025-10-23T19:36:43.61Z" },
{ url = "https://files.pythonhosted.org/packages/59/a1/1f25f8301e13751c30895092485fada29076e5e14264bdacc37202e85d24/ruff-0.14.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e681c5bc777de5af898decdcb6ba3321d0d466f4cb43c3e7cc2c3b4e7b843a05", size = 12266116, upload-time = "2025-10-23T19:36:45.625Z" },
{ url = "https://files.pythonhosted.org/packages/5c/fa/0029bfc9ce16ae78164e6923ef392e5f173b793b26cc39aa1d8b366cf9dc/ruff-0.14.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e21be42d72e224736f0c992cdb9959a2fa53c7e943b97ef5d081e13170e3ffc5", size = 12281345, upload-time = "2025-10-23T19:36:47.618Z" },
{ url = "https://files.pythonhosted.org/packages/a5/ab/ece7baa3c0f29b7683be868c024f0838770c16607bea6852e46b202f1ff6/ruff-0.14.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b8264016f6f209fac16262882dbebf3f8be1629777cf0f37e7aff071b3e9b92e", size = 12629296, upload-time = "2025-10-23T19:36:49.789Z" },
{ url = "https://files.pythonhosted.org/packages/a4/7f/638f54b43f3d4e48c6a68062794e5b367ddac778051806b9e235dfb7aa81/ruff-0.14.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5ca36b4cb4db3067a3b24444463ceea5565ea78b95fe9a07ca7cb7fd16948770", size = 13371610, upload-time = "2025-10-23T19:36:51.882Z" },
{ url = "https://files.pythonhosted.org/packages/8d/35/3654a973ebe5b32e1fd4a08ed2d46755af7267da7ac710d97420d7b8657d/ruff-0.14.2-py3-none-win32.whl", hash = "sha256:41775927d287685e08f48d8eb3f765625ab0b7042cc9377e20e64f4eb0056ee9", size = 12415318, upload-time = "2025-10-23T19:36:53.961Z" },
{ url = "https://files.pythonhosted.org/packages/71/30/3758bcf9e0b6a4193a6f51abf84254aba00887dfa8c20aba18aa366c5f57/ruff-0.14.2-py3-none-win_amd64.whl", hash = "sha256:0df3424aa5c3c08b34ed8ce099df1021e3adaca6e90229273496b839e5a7e1af", size = 13565279, upload-time = "2025-10-23T19:36:56.578Z" },
{ url = "https://files.pythonhosted.org/packages/2e/5d/aa883766f8ef9ffbe6aa24f7192fb71632f31a30e77eb39aa2b0dc4290ac/ruff-0.14.2-py3-none-win_arm64.whl", hash = "sha256:ea9d635e83ba21569fbacda7e78afbfeb94911c9434aff06192d9bc23fd5495a", size = 12554956, upload-time = "2025-10-23T19:36:58.714Z" },
{ url = "https://files.pythonhosted.org/packages/98/b8/8b6d69c57e8fe1cc6af1aa7493b72a1a5b5a8afe34694e302025441729d4/ruff-0.17.2-py3-none-linux_armv6l.whl", hash = "sha256:96a65e755d8dba094707fb23ab602111d9a31ec2835b3a6992a1914a8fcb211e", size = 10183857, upload-time = "2026-10-22T18:37:14.956Z" },
{ url = "https://files.pythonhosted.org/packages/e8/61/12cc2ff7b2fa90a268ce1e2162c23aaf76e601d22623f6a31b0eeaa764b0/ruff-0.17.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:39c5b55c960515cdb32be16e69efb5734c589c06a0eb38a22694524a336b889d", size = 10210904, upload-time = "2026-10-22T18:37:17.985Z" },
{ url = "https://files.pythonhosted.org/packages/e8/7c/b8850a8e4ad29f64a827dd6c6902db4a9aa538fb2b9eef4e26084001a3bb/ruff-0.17.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:92a80d2f44584dabb0189b3a6c393e37e525e04942838482010575cb81945be7", size = 9408439, upload-time = "2026-10-22T18:37:20.964Z" },
{ url = "https://files.pythonhosted.org/packages/b7/b6/6a64fae86ca5e5e29d5adc311e5481de726cedf9aee607435a16c31c442a/ruff-0.17.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7d0906677d89bbf3cf73cde64b5bbe2ab35df3ae43e49ee2627301e91265bac", size = 10047078, upload-time = "2026-10-22T18:37:23.717Z" },
{ url = "https://files.pythonhosted.org/packages/10/19/23d376547715175aa1bd46df48df6aacc7c6439d9daf19cac15975180aa5/ruff-0.17.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cfd783590edf2b77317882631900f6a13ea2e8eacefe6ebf2370d2715299160", size = 9990478, upload-time = "2026-10-22T18:37:26.584Z" },
{ url = "https://files.pythonhosted.org/packages/93/6c/34cda32cbe13adac5c6b291a307a5570d8ebd37c0c86339f0fe628e55632/ruff-0.17.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122258f6ecde0e0dd1bb60b24263c3edde4338caf723fca711cc729f9bfd414a", size = 10673624, upload-time = "2026-10-22T18:37:29.767Z" },
{ url = "https://files.pythonhosted.org/packages/bf/10/5d762a4dbc851d8bb47863cf3a28a61bf31c804d393b70d891fac899f5d1/ruff-0.17.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f8fba429d6ac4128ab29cfb8fa6240d2fa671d04adf7f84d3b22fc7b6b6bea4", size = 11351797, upload-time = "2026-10-22T18:37:32.535Z" },
{ url = "https://files.pythonhosted.org/packages/3f/04/a0e92a81faf5c81c1c98d4374c8401734ccf2b8cca7d0170cb0fec08c6e2/ruff-0.17.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:583bd67bb9e7061dd09bc46a0a884bae2bbd214dbcd48e509813612619683869", size = 10949414, upload-time = "2026-10-22T18:37:35.501Z" },
{ url = "https://files.pythonhosted.org/packages/c3/d8/59894cf630d2d86882457a2a6a0a1533410c5c805bfbac6f6eb4b5aef69d/ruff-0.17.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c60f93fec7142dba5b8f487f94bf2f6231dae24dc11e4c48648347c8b0720004", size = 10528000, upload-time = "2026-10-22T18:37:37.88Z" },
{ url = "https://files.pythonhosted.org/packages/ab/95/d6e30c51e851d37e10762ad91fa1e76ddaad998589b5667962a3cfbc9edd/ruff-0.17.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:c555a877f8f41447e318101cfefea9bf12dfa9f82eedf90c376e57e069f917d0", size = 10571779, upload-time = "2026-10-22T18:37:40.444Z" },
{ url = "https://files.pythonhosted.org/packages/f4/5a/a7e1d74fb464250137d3d4eca645d323536533b672004d04ee0e46e0caf3/ruff-0.17.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ba405cbd5ec7bda1f555d5e52be1fd68d02608d4d95d24591bdac92cfacb579f", size = 10051707, upload-time = "2026-10-22T18:37:42.812Z" },
{ url = "https://files.pythonhosted.org/packages/6e/f2/02fca29ca7999380f0350a7212fe755c11ab9c22d61327356cce8decdbc6/ruff-0.17.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8c21b4ab24485358f17291a4f8dc5399b834e963b8efedd920fe4fdb02081bb4", size = 9998227, upload-time = "2026-10-22T18:37:45.224Z" },
{ url = "https://files.pythonhosted.org/packages/d1/00/55aa604a093dfdc1526e570fae0bb7d37a2221c7b4728d5f080b2add16ba/ruff-0.17.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5875a16b0de70b43cb81eabea5ae5e8f5d1b2fd1b5e6ed07f80cfe5019d0bd5a", size = 10377692, upload-time = "2026-10-22T18:37:47.704Z" },
{ url = "https://files.pythonhosted.org/packages/e4/5a/a7a640f47bb906ec8fa233ea84319d503e6dee02b59e3901bd312ebd1f9b/ruff-0.17.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2d086be452eed686aa51271ecf2cbe5037956517cc6ee39a17b4489ac12f8bbe", size = 10790587, upload-time = "2026-10-22T18:37:50.191Z" },
{ url = "https://files.pythonhosted.org/packages/2c/d4/c3b74e1b4808438ea7f14bb914478025e2d51261f60774a54a0fa222ec46/ruff-0.17.2-py3-none-win32.whl", hash = "sha256:6bce7d1f5165701ae18f5df85c65afe78cddfa6c5fb4b9bdb0014d60388f3edd", size = 9870537, upload-time = "2026-10-22T18:37:52.486Z" },
{ url = "https://files.pythonhosted.org/packages/1c/9a/c7da96c2c96d20704cf9027f7a5121da272a8c05f1d1531ba949e3b3df04/ruff-0.17.2-py3-none-win_amd64.whl", hash = "sha256:1b59d447f197bef9ef48a22ba1a6016137753c6099e9bac9c36eb566e422424c", size = 10282826, upload-time = "2026-10-22T18:37:55.2Z" },
{ url = "https://files.pythonhosted.org/packages/81/60/a3604c79cb02777b6cb84da894104021986f06c4bc41375102cc95049a32/ruff-0.17.2-py3-none-win_arm64.whl", hash = "sha256:195ad918027f817c119310ca89a42d347887dfc28934aaa869325e9c5dc9d962", size = 9827036, upload-time = "2026-10-22T18:37:58.031Z" },
]
[[package]]
@@ -290,48 +298,67 @@ wheels = [
[[package]]
name = "typer"
version = "0.21.1"
version = "0.27.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "annotated-doc" },
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "rich" },
{ name = "shellingham" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/36/bf/8825b5929afd84d0dabd606c67cd57b8388cb3ec385f7ef19c5cc2202069/typer-0.21.1.tar.gz", hash = "sha256:ea835607cd752343b6b2b7ce676893e5a0324082268b48f27aa058bdb7d2145d", size = 110371, upload-time = "2026-01-06T11:21:10.989Z" }
sdist = { url = "https://files.pythonhosted.org/packages/03/51/d33db42cc72ffd8c30777547b42d01f0cbf9d95a770457698d0174b3ed71/typer-0.27.3.tar.gz", hash = "sha256:d0396f770a560ab1b0a8504e13b5f254b728cedb05c61cf0359e944e50ce8901", size = 205303, upload-time = "2026-10-06T17:24:16.61Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01", size = 47381, upload-time = "2026-01-06T11:21:09.824Z" },
]
[[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" },
{ url = "https://files.pythonhosted.org/packages/07/ea/2e31b67051e91a133189e9c000c222502ddc6969856416de0d095de4c0b0/typer-0.27.3-py3-none-any.whl", hash = "sha256:e50022f28b82a86313e54501317a1db64bf8f8d036ff8cfe5ca7e47675454aff", size = 123312, upload-time = "2026-10-06T17:24:15.054Z" },
]
[[package]]
name = "virtualenv"
version = "20.36.1"
version = "21.14.13"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "distlib" },
{ name = "filelock" },
{ name = "packaging" },
{ name = "platformdirs" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
{ name = "python-discovery" },
]
sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" }
sdist = { url = "https://files.pythonhosted.org/packages/70/aa/bff50daee0488e16d611258a9b6c5a1d0c9e128f8ca424e9f4e1c0444ef2/virtualenv-21.14.13.tar.gz", hash = "sha256:de5307ba4b9a0001d9c8ea02832d38f63ca432880f416c1bface61c4323fd905", size = 5570301, upload-time = "2026-10-22T20:10:16.952Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" },
{ url = "https://files.pythonhosted.org/packages/bb/a9/c10d8a44a29fb94a18b43fdf11f38caca53ae7c9406f11a72b1cb8f624f1/virtualenv-21.14.13-py3-none-any.whl", hash = "sha256:5fbb04138a4861580b78ba9110d529a26bc48fcc93c8ec36bc0202eeed96a788", size = 5557094, upload-time = "2026-10-22T20:10:15.096Z" },
]
[[package]]
name = "wcwidth"
version = "0.2.14"
version = "0.9.4"
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" }
sdist = { url = "https://files.pythonhosted.org/packages/ea/01/e5ac23733628a6bbb91913a765fcf55ee8070d8d991bc88fdaaf805052c7/wcwidth-0.9.4.tar.gz", hash = "sha256:fd8d57d3cdfe570e3265c8fab1885c24e34ef40889c0759c0c304a19e9835954", size = 970051, upload-time = "2026-10-24T18:11:44.954Z" }
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" },
{ url = "https://files.pythonhosted.org/packages/bf/b8/d251589a100250b71a7b8e83fedfcabfd8bd60e78cd1e3329372f63bcc93/wcwidth-0.9.4-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b7969c9aa258d536df54c8ac8af5cad0c74a6268c9972e1fb60a5f29feffb66d", size = 612952, upload-time = "2026-10-24T18:10:55.925Z" },
{ url = "https://files.pythonhosted.org/packages/04/e9/dfdca30ca36c472de3e260406dee29fc108499ec590c63b3fe279887ee9d/wcwidth-0.9.4-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:f279f32c8e206a9092fa350e53ca0c2bd8072e45b1c9fd31bb6d476b59b591c4", size = 618904, upload-time = "2026-10-24T18:11:02.008Z" },
{ url = "https://files.pythonhosted.org/packages/5d/b6/d7416f38054a107d2fb17c2b73b68723c6c334f69c4e29545fb9e6df81eb/wcwidth-0.9.4-cp310-abi3-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:a1526cd25c6420e5153bb7d764c7509e692215ee3ad9b725db1ad665d47eae09", size = 778780, upload-time = "2026-10-24T18:11:17.299Z" },
{ url = "https://files.pythonhosted.org/packages/7d/5b/42bdf9a4b0e4f9126584af2ec73642de495f3bd8add907430f13c3bb98d2/wcwidth-0.9.4-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:093469e01d881bff3db93ff3f56c364dc87c441ea01f9f41c22de0c0bdf5c6ef", size = 789012, upload-time = "2026-10-24T18:11:07.829Z" },
{ url = "https://files.pythonhosted.org/packages/fa/e8/0485850748dfdba9443f4151c7b2442cfa4987ba6cd871038eb5d6b8eca9/wcwidth-0.9.4-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b185c8b927b23af2721be1783499d1cfa5c028b098b0858951b4ae224477f1c2", size = 785946, upload-time = "2026-10-24T18:11:18.748Z" },
{ url = "https://files.pythonhosted.org/packages/e9/11/dd3d0379c6094524df6585b76ae3d9727161ff29c003177e3661c36437c1/wcwidth-0.9.4-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:17619066f95c10f44d7ae08ee456602456bc66cc9d5e91966ec73d799acebc5f", size = 795348, upload-time = "2026-10-24T18:11:09.931Z" },
{ url = "https://files.pythonhosted.org/packages/3c/48/1b5299983d9ec084be897162b7cc6014622dfe62847224fa3272b6d79ea5/wcwidth-0.9.4-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:b52f265a694dda6b3ad9050c8bd89158ad519109ef7c5eb110b906cc49f25763", size = 793420, upload-time = "2026-10-24T18:11:20.551Z" },
{ url = "https://files.pythonhosted.org/packages/7a/be/e3097d859b075344a7be57774a58d468eafb0ca88e02aad93eb31d69f6ba/wcwidth-0.9.4-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d8905f1390d828d745fdd1904374c90386d902d3cfa9ee88e9875099f75a4e17", size = 796192, upload-time = "2026-10-24T18:11:22.552Z" },
{ url = "https://files.pythonhosted.org/packages/5e/cd/739710c8cf0de461239db5bb6ee678c9a33d30aaa22beff55cc356781818/wcwidth-0.9.4-cp310-abi3-win32.whl", hash = "sha256:4831662c396dd4c59a016ec53c0ac96738006d653a00482777d9a2e2ea323d62", size = 593229, upload-time = "2026-10-24T18:11:35.609Z" },
{ url = "https://files.pythonhosted.org/packages/d6/f1/c123f902bc2ff9e9c2ab66241b8872204df87cbb18ecdf035e120860fedc/wcwidth-0.9.4-cp310-abi3-win_amd64.whl", hash = "sha256:f28d77153303450ee13ebbe888297e75cf532c754555b6cabafc9bdda16c421c", size = 597780, upload-time = "2026-10-24T18:11:36.952Z" },
{ url = "https://files.pythonhosted.org/packages/4b/88/1cb5ddaf5510b918e01aa95fc4b68df91dead784b63e4931037c5673e85f/wcwidth-0.9.4-cp310-abi3-win_arm64.whl", hash = "sha256:2b58fc8cf2de1bf9cdb68b8beaa5be198742414a7c8cd97aa51ec039783ffa49", size = 598786, upload-time = "2026-10-24T18:11:30.886Z" },
{ url = "https://files.pythonhosted.org/packages/b0/cc/46a31c12b4cdad04f546a3c0aca5d000d77aa5069ae88b4db24525cc29c4/wcwidth-0.9.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:87e4101789415a6cf55ec4c53eceeb4782c2654c88692941cb588f19dca1959f", size = 613805, upload-time = "2026-10-24T18:10:58.887Z" },
{ url = "https://files.pythonhosted.org/packages/18/f6/5a8327af9a65153ea9823d6a12fc182f325264050aa25c05a5deadc189c9/wcwidth-0.9.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ab1e5190fb82476e75fbf963e9d59b4fb1c1e1f4ced82069a0c989016aa959f7", size = 619936, upload-time = "2026-10-24T18:11:04.073Z" },
{ url = "https://files.pythonhosted.org/packages/1f/d3/bb8a7f4aa9b915e56e09841b07636ba49c82ec987ad3d2022e1696c038d9/wcwidth-0.9.4-cp314-cp314t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:8f892713ab5795d3abae181e9c02db0eb24de4084f9ee080535369d3d98d7bb5", size = 794563, upload-time = "2026-10-24T18:11:24.4Z" },
{ url = "https://files.pythonhosted.org/packages/12/0c/986acadef534688a57502d484a40e0f30bdca2d39514fe0a311f7d575f70/wcwidth-0.9.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:befbf46467100e43fd8c8cc0c031a0105f13f5418c3be95fdcf0c47214487967", size = 806565, upload-time = "2026-10-24T18:11:14.297Z" },
{ url = "https://files.pythonhosted.org/packages/9b/1d/66a94b9ac07037d7cf2c1f1288867c9932215d467893eaa5ec8d548c7f2f/wcwidth-0.9.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9c4b0dd04cca520792100f6110d10be262c94b285a281d1759c551f159214633", size = 802372, upload-time = "2026-10-24T18:11:26.275Z" },
{ url = "https://files.pythonhosted.org/packages/d1/00/1d50b8e8b45a8330b582695bf8196081a609317928a3718cf21b3d14a899/wcwidth-0.9.4-cp314-cp314t-win32.whl", hash = "sha256:14410eb73178cc492aa9bf22f7c12e3bf930e5470b7220cf6f530efbfa2e03f3", size = 598917, upload-time = "2026-10-24T18:11:38.369Z" },
{ url = "https://files.pythonhosted.org/packages/b4/fc/3e39a33fc8f5c4e07611578710275039c9d4a6655e2be7cf3459401a30cc/wcwidth-0.9.4-cp314-cp314t-win_amd64.whl", hash = "sha256:c36954849aafae1092d6f9de9548c5280c1d6841637a27ca288b2241c802c4ab", size = 603536, upload-time = "2026-10-24T18:11:39.922Z" },
{ url = "https://files.pythonhosted.org/packages/e1/8c/b1f399fd2f8449e42978192967ab6df441d7cf894b10117983c36c062b18/wcwidth-0.9.4-cp314-cp314t-win_arm64.whl", hash = "sha256:79e09e9824c88f5e275ca99e921685567a5c5c1bb6165c16e27ddd1adab33a95", size = 605127, upload-time = "2026-10-24T18:11:32.367Z" },
{ url = "https://files.pythonhosted.org/packages/04/46/839780e772e9068580619d4d691348b7a5f9c4fa5ffe501e522eefeb27cc/wcwidth-0.9.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:a8c9ce14c6b4a499e61de6de7e9e456de11be8bc991554e2ab2ed4feb40b9664", size = 613850, upload-time = "2026-10-24T18:11:00.725Z" },
{ url = "https://files.pythonhosted.org/packages/d2/66/b2559c7012bbd4268f5b5c0288cb793c78235d354e306871a55893aa9d1a/wcwidth-0.9.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:d68f09093c9bec9226a52edc47487042ed4f64c5df91d06f00419495071c0279", size = 620207, upload-time = "2026-10-24T18:11:05.975Z" },
{ url = "https://files.pythonhosted.org/packages/d1/fe/7d2c39e28924b2430b20099e36276f14e00e75c7bbe4ba5b1ed44d1d465a/wcwidth-0.9.4-cp315-cp315t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:d76b7c715c5ca0103b28915f699f1020ae21f06f1d22cdd85a7a97f7b1536eed", size = 797056, upload-time = "2026-10-24T18:11:27.911Z" },
{ url = "https://files.pythonhosted.org/packages/2b/d7/f4f9967a411e02b89b74df2f49f44977c4badccf29738f3ae6941619e27c/wcwidth-0.9.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:29c0685f5f5f8815b88308fdcbe2cd12a92996a21a5b1e18352d165174ef7a6d", size = 806169, upload-time = "2026-10-24T18:11:15.724Z" },
{ url = "https://files.pythonhosted.org/packages/ac/bb/e2b40c6522f3aa1222558487dda3551d297e07e245761b4816f6caeebc29/wcwidth-0.9.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e05458491759aa42b7b85f36345b3e6df082d6d2d9bf616299668fd598e0209d", size = 803462, upload-time = "2026-10-24T18:11:29.269Z" },
{ url = "https://files.pythonhosted.org/packages/a8/f2/ab69009f2dd0d3772d3e07f025fa046b33c60c195e4f2e5f000297d23547/wcwidth-0.9.4-cp315-cp315t-win32.whl", hash = "sha256:9e5090d253f5df99659cddfd3ed90dd21f6ff51989b0421331a63fb2681cbe27", size = 598931, upload-time = "2026-10-24T18:11:41.659Z" },
{ url = "https://files.pythonhosted.org/packages/63/6f/b321f10c2298e7d16cffc1a5f05f3d7e5231b45f9e68af5ee378e23bb2e5/wcwidth-0.9.4-cp315-cp315t-win_amd64.whl", hash = "sha256:12786620375dc83fabf4b2b2aa52416fef2bc6094522ea29c8a53c75b0dfe9b6", size = 603485, upload-time = "2026-10-24T18:11:43.052Z" },
{ url = "https://files.pythonhosted.org/packages/4e/cc/7eeb28059186cae72a770df79e744f232ef068192a5a588b718473e35bb4/wcwidth-0.9.4-cp315-cp315t-win_arm64.whl", hash = "sha256:2cca7b5d0b9d0ae49068d3ce806e965326a987f555b62eaa322223396e10d01b", size = 605198, upload-time = "2026-10-24T18:11:33.75Z" },
{ url = "https://files.pythonhosted.org/packages/6f/0c/309b1813b230d1dbbd6f2c2ab2bb21ef68992358977605d9ef289bbfedcc/wcwidth-0.9.4-py3-none-any.whl", hash = "sha256:901eace6d2dd25ebb917d0ce5db18e5c537e1504c905cf32f0d006c12098b05d", size = 301806, upload-time = "2026-10-24T18:10:52.651Z" },
]