diff --git a/.ruff.toml b/.ruff.toml index 1bf7eb5..d684042 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -66,6 +66,8 @@ ignore = [ "RUF003", # Ambiguous emdash/endash "SIM108", # Consider ternary operator "SLF001", # Private member accessed + "TD002", # Missing author in TODO + "TD003", # Missing issue link in TODO "TRY003", # Avoid specifying long messages outside the exception class ] @@ -87,7 +89,8 @@ ignore = [ "T201", # Print statements ] "python/tests/**.py" = [ - "D", # Docstrings + "D", # Docstrings rules + "PLR2004", # Magic value used in comparison "S101", # Use of assert detected ] diff --git a/pyproject.toml b/pyproject.toml index ea43d4c..44059c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,6 +50,7 @@ dynamic = ["version"] dependencies = [ "click>=8.1.8", "deepmerge>=2.0", + "jinja2>=3.1", "markdown>=3.7", "pygments>=2.20", "pymdown-extensions>=10.21.2", @@ -70,7 +71,7 @@ zensical = "zensical.main:cli" [dependency-groups] dev = [ "maturin>=1.10.2", - "pytest>=8.0", + "pytest>=9.0.3", "ruff>=0.12.8", "ty>=0.0.32", "types-pyyaml>=6.0.12", diff --git a/python/tests/unit/extensions/test_macros.py b/python/tests/unit/extensions/test_macros.py new file mode 100644 index 0000000..397c7e8 --- /dev/null +++ b/python/tests/unit/extensions/test_macros.py @@ -0,0 +1,407 @@ +# Copyright (c) 2025-2026 Zensical and contributors + +# SPDX-License-Identifier: MIT +# All contributions are certified under the DCO + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +import pytest +from jinja2.exceptions import TemplateSyntaxError, UndefinedError +from markdown import Markdown + +from zensical.extensions.context import ( + ContextExtension, + ContextPreprocessor, + Page, +) +from zensical.extensions.emoji import to_svg, twemoji +from zensical.extensions.macros import ( + MacroEnv, + MacrosExtension, + _fix_url, + _format_value, + _load_module, + _load_one_yaml, + _make_table, + _merge_include_yaml, + _pretty, +) + +if TYPE_CHECKING: + from pathlib import Path + + +MINIMAL_EXTENSIONS = {} + +RECOMMENDED_EXTENSIONS = { + "abbr": {}, + "admonition": {}, + "attr_list": {}, + "def_list": {}, + "footnotes": {}, + "md_in_html": {}, + "toc": {"permalink": True}, + "pymdownx.arithmatex": {"generic": True}, + "pymdownx.betterem": {}, + "pymdownx.caret": {}, + "pymdownx.details": {}, + "pymdownx.emoji": { + "emoji_generator": to_svg, + "emoji_index": twemoji, + }, + "pymdownx.highlight": { + "anchor_linenums": True, + "line_spans": "__span", + "pygments_lang_class": True, + }, + "pymdownx.inlinehilite": {}, + "pymdownx.keys": {}, + "pymdownx.magiclink": {}, + "pymdownx.mark": {}, + "pymdownx.smartsymbols": {}, + "pymdownx.superfences": { + "custom_fences": [{"name": "mermaid", "class": "mermaid"}] + }, + "pymdownx.tabbed": { + "alternate_style": True, + "combine_header_slug": True, + }, + "pymdownx.tasklist": {"custom_checkbox": True}, + "pymdownx.tilde": {}, +} + + +def _page(meta: dict[str, Any] | None = None) -> dict[str, Any]: + return { + "url": "/", + "path": "index.md", + "meta": dict(meta or {}), + } + + +@pytest.fixture( + name="project_config", + params=[MINIMAL_EXTENSIONS, RECOMMENDED_EXTENSIONS], + ids=["minimal_markdown", "recommended_markdown"], +) +def _fixture_project_config(request: pytest.FixtureRequest) -> dict[str, Any]: + active_markdown = dict(request.param) + return {"site_name": "Demo", "markdown_extensions": active_markdown} + + +@pytest.fixture(name="md") +def _fixture_md( + project_config: dict[str, Any], + request: pytest.FixtureRequest, + tmp_path: Path, +) -> Markdown: + """Return a Markdown instance with MacrosExtension registered.""" + fixture_param = dict(getattr(request, "param", {})) + allowed_keys = {"macros", "page", "project_config"} + unexpected_keys = set(fixture_param) - allowed_keys + if unexpected_keys: + raise ValueError( + f"Unsupported md fixture params: {sorted(unexpected_keys)}. " + "Use only 'macros', 'page', and 'project_config'." + ) + macro_config = dict(fixture_param.get("macros", {})) + page = fixture_param.get("page", Page(url="/", path="index.md")) + if isinstance(page, dict): + page = Page(**page) + project_overrides = dict(fixture_param.get("project_config", {})) + effective_project_config = {**project_config, **project_overrides} + if "root_dir" not in effective_project_config: + effective_project_config["root_dir"] = str(tmp_path) + markdown_extensions = dict( + effective_project_config.get("markdown_extensions", {}) + ) + md = Markdown( + extensions=list(markdown_extensions.keys()), + extension_configs=markdown_extensions, + ) + ContextExtension(page=page, config=effective_project_config).extendMarkdown( + md + ) + MacrosExtension(**macro_config).extendMarkdown(md) + return md + + +@pytest.mark.parametrize( + ("url", "expected"), + [ + ("page.html", "../page.html"), + ("assets/img.png", "../assets/img.png"), + ("https://example.org", "https://example.org"), + ("mailto:test@example.org", "mailto:test@example.org"), + ], +) +def test_fix_url(url: str, expected: str) -> None: + assert _fix_url(url) == expected + + +def test_macro_env_registers_macros_and_filters() -> None: + env = MacroEnv() + + @env.macro + def twice(value: int) -> int: + return value * 2 + + @env.filter(name="rev") + def reverse(value: str) -> str: + return value[::-1] + + assert env.macros["twice"](4) == 8 + assert env.filters["rev"]("abc") == "cba" + + +@pytest.mark.parametrize( + ("payload", "expected"), + [ + ([("alpha", "str", "hello")], "**alpha** | *str* | hello"), + ([], ""), + ], +) +def test_pretty(payload: list[tuple[str, str, str]], expected: str) -> None: + output = _pretty(payload) + if expected: + assert expected in output + else: + assert output == "" + + +def test_make_table_escapes_pipe() -> None: + table = _make_table( + rows=[("left|right", "type", "a|b")], + header=("Variable", "Type", "Content"), + ) + assert "left\\|right" in table + assert "a\\|b" in table + + +def test_format_value_for_callable_and_dict() -> None: + def sample(name: str) -> str: + """Doc first line.\nIgnored.""" + return name + + class Obj: + pass + + value = { + "count": 1, + "name": "hello", + "obj": Obj(), + } + + rendered_callable = _format_value(sample) + rendered_dict = _format_value(value) + + assert "(*name*)" in rendered_callable + assert "Doc first line." in rendered_callable + assert "**count** = 1" in rendered_dict + assert "**obj** [*Obj*]" in rendered_dict + + +@pytest.mark.parametrize( + ("relative_path", "content", "expected"), + [ + ("ok.yaml", "a: 1\nb: x\n", {"a": 1, "b": "x"}), + ("not_dict.yaml", "- 1\n- 2\n", None), + ], +) +def test_load_one_yaml_with_relative_paths( + tmp_path: Path, + relative_path: str, + content: str, + expected: dict | None, +) -> None: + (tmp_path / relative_path).write_text(content, encoding="utf-8") + loaded = _load_one_yaml(relative_path, tmp_path) + assert loaded == expected + + +def test_load_one_yaml_blocks_outside_project_root(tmp_path: Path) -> None: + outside = tmp_path.parent / "outside.yaml" + outside.write_text("x: 1\n", encoding="utf-8") + loaded = _load_one_yaml(str(outside), tmp_path) + assert loaded is None + + +@pytest.mark.parametrize( + "include_yaml", + [ + ["a.yaml", "b.yaml"], + {"left": "a.yaml", "right": "b.yaml"}, + ], +) +def test_merge_include_yaml_list_and_dict( + tmp_path: Path, + include_yaml: list[str] | dict[str, str], +) -> None: + (tmp_path / "a.yaml").write_text("x: 1\n", encoding="utf-8") + (tmp_path / "b.yaml").write_text("y: 2\n", encoding="utf-8") + variables: dict = {} + _merge_include_yaml(include_yaml, tmp_path, variables) + if isinstance(include_yaml, list): + assert variables == {"x": 1, "y": 2} + else: + assert variables == {"left": {"x": 1}, "right": {"y": 2}} + + +def test_load_module_from_local_file(tmp_path: Path) -> None: + (tmp_path / "main.py").write_text( + "def define_env(env):\n" + " env.variables['site_name'] = 'Demo'\n" + " @env.macro\n" + " def twice(x):\n" + " return x * 2\n" + " @env.filter\n" + " def shout(s):\n" + " return s.upper()\n", + encoding="utf-8", + ) + variables, macros, filters = _load_module("main", tmp_path) + assert variables["site_name"] == "Demo" + assert macros["twice"](3) == 6 + assert filters["shout"]("hi") == "HI" + + +@pytest.mark.parametrize("module_name", ["../evil", "foo/bar", r"foo\\bar"]) +def test_load_module_rejects_non_package_like_module_names( + module_name: str, +) -> None: + assert _load_module(module_name) == ({}, {}, {}) + + +@pytest.mark.parametrize( + ("md", "expected"), + [ + ({"macros": {"render_by_default": False}}, "

Value: {{ 1 + 1 }}

"), + ({"macros": {"render_by_default": True}}, "

Value: 2\n

"), + ], + indirect=["md"], +) +def test_preprocessor_respects_render_by_default( + md: Markdown, expected: str +) -> None: + source = "Value: {{ 1 + 1 }}" + assert md.convert(source) == expected + + +@pytest.mark.parametrize( + "md", + [ + { + "macros": {"render_by_default": False}, + "page": _page({"render_macros": True}), + }, + ], + indirect=True, +) +def test_preprocessor_renders_when_opted_in_by_page_meta(md: Markdown) -> None: + assert md.convert("Value: {{ 1 + 1 }}") == "

Value: 2\n

" + + +@pytest.mark.parametrize( + "md", + [ + { + "macros": { + "render_by_default": True, + "include_yaml": ["vars.yaml"], + "module_name": "main", + }, + "page": _page({"render_macros": True}), + "project_config": {"extra": {"who": "world"}}, + }, + ], + indirect=True, +) +def test_preprocessor_renders_with_include_yaml_and_module( + md: Markdown, + tmp_path: Path, +) -> None: + (tmp_path / "vars.yaml").write_text("name: Ada\n", encoding="utf-8") + (tmp_path / "main.py").write_text( + "def define_env(env):\n" + " @env.macro\n" + " def greet(name):\n" + " return f'Hello {name}!'\n", + encoding="utf-8", + ) + rendered = md.convert("{{ greet(name) }}\n\n{{ who }}") + assert "Hello Ada!" in rendered + assert "world" in rendered + + +@pytest.mark.parametrize( + "md", + [{"macros": {"render_by_default": True}}], + indirect=True, +) +def test_preprocessor_error_handling_keep_text_by_default(md: Markdown) -> None: + assert "{{ not_closed" in md.convert("{{ not_closed") + + +@pytest.mark.parametrize( + "md", + [{"macros": {"render_by_default": True, "on_error_fail": True}}], + indirect=True, +) +def test_preprocessor_error_handling_raises_when_enabled(md: Markdown) -> None: + with pytest.raises(TemplateSyntaxError): + md.convert("{{ not_closed") + + +@pytest.mark.parametrize( + "md", + [ + { + "macros": {"render_by_default": True}, + "page": _page({"render_macros": True, "title": "Doc {{ 2 + 3 }}"}), + }, + ], + indirect=True, +) +def test_preprocessor_renders_jinja_in_title_meta(md: Markdown) -> None: + md.convert("# {{ title }}") + context = ContextPreprocessor.from_markdown(md) + assert context is not None + assert context.page.meta["title"] == "Doc 5" + + +@pytest.mark.parametrize( + "md", + [ + { + "macros": { + "render_by_default": True, + "on_undefined": "strict", + "on_error_fail": True, + }, + } + ], + indirect=True, +) +def test_preprocessor_strict_undefined_raises(md: Markdown) -> None: + with pytest.raises(UndefinedError): + md.convert("{{ missing_variable }}") diff --git a/python/zensical/config.py b/python/zensical/config.py index 091eac9..f813140 100644 --- a/python/zensical/config.py +++ b/python/zensical/config.py @@ -43,7 +43,7 @@ from yaml.constructor import ConstructorError from zensical.compat.autorefs import get_autorefs_extension from zensical.compat.mkdocstrings import get_mkdocstrings_extension -from zensical.extensions import glightbox +from zensical.extensions import glightbox, macros from zensical.extensions.emoji import to_svg, twemoji if TYPE_CHECKING: @@ -593,10 +593,17 @@ def _apply_defaults(config: dict, path: str) -> dict: ) ) + # Map macros plugin configuration to the extension configuration + if "macros" in config["plugins"]: + plugin = config["plugins"]["macros"]["config"] + config["markdown_extensions"].append(macros.MacrosExtension.name) + config["mdx_configs"][macros.MacrosExtension.name] = plugin + # List files along with their hashes, so we can rebuild when they change config["watched_files"] = sorted( _list_sources(config, path) # mkdocstrings | _list_snippet_files(config, path) # pymdownx.snippets + | _list_macros_files(config, path) # macros ) # Hash all templates, so we rebuild if something changes @@ -681,15 +688,13 @@ def _list_sources(config: dict, config_file: str) -> set[tuple[str, int]]: path = root.joinpath(python_path).resolve() if path.is_dir() and path.is_relative_to(root) and path != root: for py_module in _list_py_modules(path): - files_with_hash.add( # noqa: PERF401 + files_with_hash.add( (str(py_module), int(os.path.getmtime(py_module))) ) return files_with_hash -def _list_snippet_files( - config: dict, config_file: str -) -> set[tuple[str, int]]: +def _list_snippet_files(config: dict, config_file: str) -> set[tuple[str, int]]: """List files referenced in pymdownx.snippets auto_append configuration.""" snippets_config = config["mdx_configs"].get("pymdownx.snippets", {}) auto_append = snippets_config.get("auto_append", []) @@ -708,6 +713,53 @@ def _list_snippet_files( return files_with_mtime +def _list_macros_files(config: dict, config_file: str) -> set[tuple[str, int]]: + """List files referenced in macros plugin/extension.""" + root = Path(config_file).parent.resolve() + macros_config = config["mdx_configs"].get(macros.MacrosExtension.name, {}) + macros_files = [] + files_with_mtime = set() + + module = macros_config.get("module", "main") + if (module_path := root.joinpath(module + ".py").resolve()).is_file(): + macros_files.append(module_path) + + pluglets = macros_config.get("modules", []) + for pluglet in pluglets: + try: + pluglet_module = importlib.import_module(pluglet) + except ImportError: # noqa: PERF203 + continue + else: + macros_files.append(pluglet_module.__file__) + + include_yaml: list[str] | dict[str, str] = macros_config.get( + "include_yaml", [] + ) + if isinstance(include_yaml, dict): + include_yaml = list(include_yaml.values()) + for yaml_file in include_yaml: + candidate = root.joinpath(yaml_file).resolve() + if candidate.is_file(): + macros_files.append(candidate) + + for file_path in macros_files: + mtime = int(os.path.getmtime(file_path)) + files_with_mtime.add((str(file_path), mtime)) + + include_dir = macros_config.get("include_dir", None) + if include_dir: + candidate_dir = root.joinpath(include_dir).resolve() + if candidate_dir.is_dir(): + for root, _, files in os.walk(candidate_dir): + for file in files: + file_path = os.path.join(root, file) + mtime = int(os.path.getmtime(file_path)) + files_with_mtime.add((file_path, mtime)) + + return files_with_mtime + + def _list_templates(config: dict) -> list[tuple[str, int]]: """List all template files in the theme directories.""" # Collect file paths and their mtimes diff --git a/python/zensical/extensions/macros.py b/python/zensical/extensions/macros.py new file mode 100644 index 0000000..2006697 --- /dev/null +++ b/python/zensical/extensions/macros.py @@ -0,0 +1,654 @@ +# Copyright (c) 2025-2026 Zensical and contributors + +# SPDX-License-Identifier: MIT +# All contributions are certified under the DCO + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +from __future__ import annotations + +import importlib.util +import inspect +import platform +import subprocess +import traceback +from collections.abc import Callable, Iterable +from dataclasses import asdict, dataclass, field +from datetime import datetime +from functools import cache +from pathlib import Path +from typing import TYPE_CHECKING, Any, Literal, TypeAlias +from urllib.parse import urlparse + +import jinja2 +import yaml +from jinja2.exceptions import UndefinedError +from markdown import Extension +from markdown.preprocessors import Preprocessor + +from zensical.extensions.context import ContextPreprocessor + +if TYPE_CHECKING: + from jinja2 import Environment + from markdown import Markdown + + +# ----------------------------------------------------------------------------- +# Constants +# ----------------------------------------------------------------------------- + + +VariablesType: TypeAlias = dict[str, Any] +MacrosType: TypeAlias = dict[str, Callable[..., Any]] +FiltersType: TypeAlias = dict[str, Callable[..., Any]] +VariablesMacrosFiltersType: TypeAlias = tuple[ + VariablesType, MacrosType, FiltersType +] + +_MACROS_INFO = """ +{# +Template for the macro_info() command +(C) Laurent Franceschetti 2019 +#} + +## Macros Plugin Environment + +### General List + +All available variables and filters within the macros plugin: + +{{ context() | pretty }} + +### Config Information + +Standard configuration information. Do not try to modify. + +e.g. {{ "`{{ config.docs_dir }}`" }} + +See also the [MkDocs documentation on the config object](https://www.MkDocs.org/user-guide/custom-themes/#config). + +{{ context(config)| pretty }} + +### Macros + +These macros have been defined programmatically for this environment +(module or pluglets). + +{{ context(macros)| pretty }} + +### Git Information + +Information available on the last commit and the git repository containing the +documentation project: + +e.g. {{ "`{{ git.message }}`" }} + +{{ context(git)| pretty }} + +### Page Attributes + +Provided by MkDocs. These attributes change for every page +(the attributes shown are for this page). + +e.g. {{ "`{{ page.title }}`" }} + +See also the [MkDocs documentation on the page object](https://www.MkDocs.org/user-guide/custom-themes/#page). + +{{ context(page)| pretty }} + +To have all titles of all pages, use: + + {% raw %} + {% for page in navigation.pages %} + - {{ page.title }} + {% endfor %} + {% endraw %} + + +### Plugin Filters + +These filters are provided as a standard by the macros plugin. + +{{ context(filters)| pretty }} + +### Builtin Jinja2 Filters + +These filters are provided by Jinja2 as a standard. + +See also the [Jinja2 documentation on builtin filters](https://jinja.palletsprojects.com/en/3.1.x/templates/#builtin-filters). + +{{ context(filters_builtin) | pretty }} +""" + + +# ----------------------------------------------------------------------------- +# Classes +# ----------------------------------------------------------------------------- + + +class MacroEnv: + """Minimal env object for compatibility with MkDocs Macros.""" + + def __init__(self) -> None: + self.variables: VariablesType = {} + self.macros: MacrosType = {} + self.filters: FiltersType = {} + + def macro( + self, fn: Callable[..., Any] | None = None, name: str | None = None + ) -> Any: + """Register a macro. + + Use as `@env.macro` or `env.macro(func)` or `env.macro(func, 'name')`. + """ + if fn is None: + return lambda f: self.macro(f, name) + self.macros[name or fn.__name__] = fn # ty:ignore[unresolved-attribute] + return fn + + def filter( + self, fn: Callable[..., Any] | None = None, name: str | None = None + ) -> Any: + """Register a filter. + + Use as `@env.filter` or `env.filter(func)`. + """ + if fn is None: + return lambda f: self.filter(f, name) + self.filters[name or fn.__name__] = fn # ty:ignore[unresolved-attribute] + return fn + + +@dataclass +class MacrosConfig: + """Configuration for the macros Markdown extension.""" + + module_name: str = "main" + modules: list[str] = field(default_factory=list) + include_yaml: list[str] | dict[str, str] = field(default_factory=list) + include_dir: str = "" + render_by_default: bool = True + on_error_fail: bool = False + on_undefined: Literal["keep", "strict"] = "keep" + verbose: bool = False + j2_block_start_string: str = "{%" + j2_block_end_string: str = "%}" + j2_variable_start_string: str = "{{" + j2_variable_end_string: str = "}}" + j2_comment_start_string: str = "{#" + j2_comment_end_string: str = "#}" + j2_extensions: list[str] = field(default_factory=list) + + +class MacrosPreprocessor(Preprocessor): + """Build Jinja2 context, render body.""" + + name = "macros" + + def __init__( + self, + md: Markdown, + *, + config: MacrosConfig, + ) -> None: + self.md: Markdown = md + self.config = config + + def run(self, lines: list[str]) -> list[str]: + """Render body as Jinja2 template with built context.""" + # Fetch rendering context from our context preprocessor + context = ContextPreprocessor.from_markdown(self.md) + page = context.page if context else None + project_config = context.config if context else {} + project_root = Path(project_config.get("root_dir", ".")).resolve() + + # Don't render if not enabled by default and no page-level override + if ( + not self.config.render_by_default + and (not page or (page and not page.meta.get("render_macros"))) + ) or (page and page.meta.get("render_macros") is False): + return lines + + text = "\n".join(lines) + variables = {} + macros = {} + filters: dict[str, Callable] = { + "pretty": _pretty, + "fix_url": _fix_url, + } + + # Merge extra into variables + if extra := project_config.get("extra"): + variables["extra"] = extra + variables.update(extra) + + # Load YAML from configuration + _merge_include_yaml( + self.config.include_yaml, + project_root, + variables, + ) + + # Load YAML data from page metadata + if page: + _merge_include_yaml( + page.meta.get("include_yaml", []), + project_root, + variables, + ) + + # Load module. + # Relative path (without extension) or importable module name + if self.config.module_name: + mod_vars, mod_macros, mod_filters = _load_module( + self.config.module_name, + project_root, + ) + variables.update(mod_vars) + macros.update(mod_macros) + filters.update(mod_filters) + + # Load pluglets (preinstalled modules) + # Importable module names only + for plug in self.config.modules: + plug_vars, plug_macros, plug_filters = _load_module(plug) + variables.update(plug_vars) + macros.update(plug_macros) + filters.update(plug_filters) + + # Merge page metadata + if page: + variables.update(page.meta) + + # Build Jinja2 environment + env_kw: dict[str, Any] = { + "block_start_string": self.config.j2_block_start_string, + "block_end_string": self.config.j2_block_end_string, + "variable_start_string": self.config.j2_variable_start_string, + "variable_end_string": self.config.j2_variable_end_string, + } + if self.config.j2_comment_start_string is not None: + env_kw["comment_start_string"] = self.config.j2_comment_start_string + if self.config.j2_comment_end_string is not None: + env_kw["comment_end_string"] = self.config.j2_comment_end_string + if self.config.on_undefined == "strict": + env_kw["undefined"] = jinja2.StrictUndefined + if self.config.j2_extensions: + env_kw["extensions"] = self.config.j2_extensions + + if ( + self.config.include_dir + and ( + include_dir_path := project_root / self.config.include_dir + ).exists() + ): + env_kw["loader"] = jinja2.FileSystemLoader(include_dir_path) + + env = jinja2.Environment(**env_kw) # noqa: S701 + + # Store builtin Jinja filters before adding our own + builtin_filters = env.filters.copy() + + # Add user macros and filters to the environment + env.filters.update(filters) + env.globals.update(macros) + + # Add our own macros and variables to the environment + env_globals: dict[str, Any] = { + "config": project_config, + "context": _context_closure(variables), + "environment": _get_env_info(), + "files": [], + "filters": filters, + "filters_builtin": builtin_filters, + "git": _get_git_info(), + "macros": macros, + "navigation": [], + "now": _now, + "plugin": asdict(self.config), + } + if page: + env_globals["page"] = page + env.globals.update(env_globals) + # This copies the environment filters and globals + # into a new environment so this call must be last + env.globals["macros_info"] = _macros_info_closure(env) # ty:ignore[invalid-assignment] + + # Make global variables accessible to `context()` macro + variables.update(env.globals) + + # Render title if it contains Jinja2 syntax + title = variables.get("title") + if isinstance(title, str) and ( + self.config.j2_variable_start_string in title + or self.config.j2_block_start_string in title + ): + try: + title_template = env.from_string(title) + new_title = title_template.render(**variables) + except Exception: # noqa: BLE001 + pass + else: + variables["title"] = new_title + if page: + page.meta["title"] = new_title + + # Render body and return it + try: + template = env.from_string(text) + rendered = template.render(**variables) + except Exception: + if self.config.on_error_fail: + raise + rendered = text + return rendered.split("\n") + + +class MacrosExtension(Extension): + """Jinja2 templating with variables, macros, and filters.""" + + name = "zensical.extensions.macros" + + def __init__(self, **kwargs: Any) -> None: + self._kwargs: dict[str, Any] = kwargs + + def extendMarkdown(self, md: Markdown) -> None: + md.registerExtension(self) + config = MacrosConfig(**self._kwargs) + md.preprocessors.register( + MacrosPreprocessor(md, config=config), + MacrosPreprocessor.name, + priority=20, + ) + + +# ----------------------------------------------------------------------------- +# Functions +# ----------------------------------------------------------------------------- + + +def makeExtension(**kwargs: Any) -> MacrosExtension: + """Register Markdown extension.""" + return MacrosExtension(**kwargs) + + +def _now() -> datetime: + """Return current datetime (`datetime.now()`).""" + return datetime.now() # noqa: DTZ005 + + +def _macros_info_closure(env: Environment) -> Callable[[], str]: + new_env = jinja2.Environment() # noqa: S701 + new_env.filters.update(env.filters) + new_env.globals.update(env.globals) + + def macros_info() -> str: + """Display info about the macros environment, for debugging purposes.""" + return new_env.from_string(_MACROS_INFO).render() + + return macros_info + + +def _fix_url(url: str) -> str: + parsed = urlparse(url) + if (not parsed.scheme) and parsed.path: + return "../" + url + return url + + +def _list_items(obj: Any) -> Iterable[tuple[str | int, Any]]: + try: + return sorted(obj.items()) + except AttributeError: + return sorted(obj.__dict__.items()) + except TypeError: + return enumerate(list(obj)) + + +def _format_value(value: Any) -> str: + if callable(value): + if doc := value.__doc__: + doc = doc.strip().split("\n", 1)[0] + else: + return "" + try: + param_names = ", ".join(inspect.signature(value).parameters) + except ValueError: + return doc + else: + return f"(*{param_names}*)
{doc}" if param_names else doc + elif isinstance(value, dict): + r_list = [] + for key, val in _list_items(value): + if isinstance(val, (int, float, str, list, dict)) or val is None: + r_list.append(f"**{key}** = {val!r}") + else: + r_list.append(f"**{key}** [*{type(val).__name__}*]") + return ", ".join(r_list) + else: + return repr(value) + + +def _context_closure( + variables: dict[str, Any], +) -> Callable[[Any], list[tuple[Any, Any, str]]]: + def context(obj: Any = None) -> list[tuple[Any, Any, str]]: + """Display macros context (single object or all context).""" + if obj is None: + obj = variables + try: + return [ + (var, type(value).__name__, _format_value(value)) + for var, value in _list_items(obj) + ] + except UndefinedError as e: + return [("*Error!*", type(e).__name__, str(e))] + except AttributeError: + # Not an object or dictionary (int, str, etc.) + return [(obj, type(obj).__name__, repr(obj))] + + return context + + +def _make_table( + rows: list[tuple[str, str, str]], + header: tuple[str, str, str], +) -> str: + def _escape_cell(value: str) -> str: + return value.replace("|", r"\|") + + header_line = " | ".join(_escape_cell(item) for item in header) + separator_line = " | ".join(["---"] * len(header)) + body_lines = [ + " | ".join(_escape_cell(str(item)) for item in row) for row in rows + ] + return "\n".join([header_line, separator_line, *body_lines]) + + +def _pretty(var_list: list[Any]) -> str: + if not var_list: + return "" + rows = [ + ( + f"**{var}**", + f"*{var_type}*", + content.replace("\n", "
"), + ) + for var, var_type, content in var_list + ] + try: + return _make_table(rows, ("Variable", "Type", "Content")) + except Exception as error: # noqa: BLE001 + return f"#{type(error).__name__}: {error}\n{traceback.format_exc()}" + + +def _load_module( + module_name: str, project_root: Path | None = None +) -> VariablesMacrosFiltersType: + """Load a module by name (e.g. 'main').""" + if project_root: + for candidate in [ + project_root / f"{module_name}.py", + project_root / module_name / "__init__.py", + ]: + if not candidate.exists() or not candidate.is_relative_to( + project_root + ): + continue + spec = importlib.util.spec_from_file_location( + module_name, candidate + ) + if spec and spec.loader: + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + if hasattr(mod, "define_env"): + env = MacroEnv() + mod.define_env(env) + return env.variables, env.macros, env.filters + break + + # Only try import for package-like names (no path separators or ".."). + if "/" in module_name or "\\" in module_name or ".." in module_name: + return {}, {}, {} + try: + mod = importlib.import_module(module_name) + except ImportError: + pass + else: + if hasattr(mod, "define_env"): + env = MacroEnv() + mod.define_env(env) + return env.variables, env.macros, env.filters + return {}, {}, {} + + +def _load_one_yaml( + path: str, + project_root: Path, +) -> VariablesType | None: + """Load a single YAML file. Path must be relative to project root.""" + p = ( + (project_root / path).resolve() + if not Path(path).is_absolute() + else Path(path).resolve() + ) + if not p.exists() or not p.is_relative_to(project_root): + return None + try: + with open(p, encoding="utf-8") as f: + data = yaml.safe_load(f) + except (OSError, ValueError): + return None + else: + return data if isinstance(data, dict) else None + + +def _merge_include_yaml( + include_yaml: list[str] | dict[str, str], + project_root: Path, + variables: VariablesType, +) -> None: + """Merge external data into variables.""" + if not include_yaml: + return + if isinstance(include_yaml, dict): + for key, path in include_yaml.items(): + if data := _load_one_yaml(path, project_root): + variables[key] = data + else: + for path in include_yaml: + if data := _load_one_yaml(path, project_root): + variables.update(data) + + +@cache +def _get_git_info() -> dict[str, Any]: + """Return Git metadata for the current repository.""" + commands: dict[str, tuple[str, ...]] = { + "short_commit": ("git", "rev-parse", "--short", "HEAD"), + "commit": ("git", "rev-parse", "HEAD"), + "tag": ("git", "describe", "--tags"), + # With --abbrev set to 0, git finds the nearest tag name without suffix + "short_tag": ("git", "describe", "--tags", "--abbrev=0"), + "author": ("git", "log", "-1", "--pretty=format:%an"), + "author_email": ("git", "log", "-1", "--pretty=format:%ae"), + "committer": ("git", "log", "-1", "--pretty=format:%cn"), + "committer_email": ( + "git", + "log", + "-1", + "--pretty=format:%ce", + ), + # %cI is strict ISO 8601 commit date + "date_ISO": ("git", "log", "-1", "--pretty=format:%cI"), + "message": ("git", "log", "-1", "--pretty=format:%B"), + "raw": ("git", "log", "-1"), + "root_dir": ("git", "rev-parse", "--show-toplevel"), + } + + result: dict[str, Any] = {"status": False, "date": None} + + for field_name, git_command in commands.items(): + try: + output = subprocess.check_output( # noqa: S603 + git_command, + text=True, + stderr=subprocess.DEVNULL, + ).strip() + except FileNotFoundError as error: # noqa: PERF203 + # Git executable not found, abort early. + return { + "status": False, + "diagnosis": "Git command not found", + "error": str(error), + "date": None, + } + except subprocess.CalledProcessError as error: + if error.returncode == 128: # noqa: PLR2004 + # Usually means no git repo or no tags. + result[field_name] = "" + else: + result[field_name] = ( + f"# Cannot execute '{git_command}': {error}" + ) + except Exception as error: # noqa: BLE001 + result[field_name] = f"# Unexpected error '{git_command}': {error}" + else: + result[field_name] = output + if field_name == "date_ISO": + result["date"] = datetime.fromisoformat( + output.replace("Z", "+00:00") + ) + result["status"] = True + + return result + + +@cache +def _get_env_info() -> dict[str, str]: + sys_name = platform.system() or "" + sys_name = {"Darwin": "MacOs"}.get(sys_name, sys_name) + return { + "system": sys_name, + "system_version": platform.release(), + "python_version": platform.python_version(), + "mkdocs_version": "1.6.1", + "macros_plugin_version": "1.3.7", + "jinja2_version": jinja2.__version__ if jinja2 else "0.0.0", + } diff --git a/python/zensical/markdown/render.py b/python/zensical/markdown/render.py index 600c60a..aa4b6d5 100644 --- a/python/zensical/markdown/render.py +++ b/python/zensical/markdown/render.py @@ -127,7 +127,7 @@ def render(content: str, path: str, url: str) -> dict: if meta.get("search", {}).get("exclude", False): search_processor.data = [] - # Sanitize metadata before passing it to Rust. + # Sanitize metadata before passing it to Rust meta = {k: _sanitize(v) for k, v in meta.items()} # Return Markdown with metadata diff --git a/uv.lock b/uv.lock index 75b2cb2..8812cac 100644 --- a/uv.lock +++ b/uv.lock @@ -53,6 +53,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + [[package]] name = "markdown" version = "3.10.2" @@ -62,6 +74,91 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, ] +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + [[package]] name = "maturin" version = "1.13.1" @@ -210,27 +307,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.11" +version = "0.15.12" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/8d/192f3d7103816158dfd5ea50d098ef2aec19194e6cbccd4b3485bdb2eb2d/ruff-0.15.11.tar.gz", hash = "sha256:f092b21708bf0e7437ce9ada249dfe688ff9a0954fc94abab05dcea7dcd29c33", size = 4637264, upload-time = "2026-04-16T18:46:26.58Z" } +sdist = { url = "https://files.pythonhosted.org/packages/99/43/3291f1cc9106f4c63bdce7a8d0df5047fe8422a75b091c16b5e9355e0b11/ruff-0.15.12.tar.gz", hash = "sha256:ecea26adb26b4232c0c2ca19ccbc0083a68344180bba2a600605538ce51a40a6", size = 4643852, upload-time = "2026-04-24T18:17:14.305Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/1e/6aca3427f751295ab011828e15e9bf452200ac74484f1db4be0197b8170b/ruff-0.15.11-py3-none-linux_armv6l.whl", hash = "sha256:e927cfff503135c558eb581a0c9792264aae9507904eb27809cdcff2f2c847b7", size = 10607943, upload-time = "2026-04-16T18:46:05.967Z" }, - { url = "https://files.pythonhosted.org/packages/e7/26/1341c262e74f36d4e84f3d6f4df0ac68cd53331a66bfc5080daa17c84c0b/ruff-0.15.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7a1b5b2938d8f890b76084d4fa843604d787a912541eae85fd7e233398bbb73e", size = 10988592, upload-time = "2026-04-16T18:46:00.742Z" }, - { url = "https://files.pythonhosted.org/packages/03/71/850b1d6ffa9564fbb6740429bad53df1094082fe515c8c1e74b6d8d05f18/ruff-0.15.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d4176f3d194afbdaee6e41b9ccb1a2c287dba8700047df474abfbe773825d1cb", size = 10338501, upload-time = "2026-04-16T18:46:03.723Z" }, - { url = "https://files.pythonhosted.org/packages/f2/11/cc1284d3e298c45a817a6aadb6c3e1d70b45c9b36d8d9cce3387b495a03a/ruff-0.15.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b17c886fb88203ced3afe7f14e8d5ae96e9d2f4ccc0ee66aa19f2c2675a27e4", size = 10670693, upload-time = "2026-04-16T18:46:41.941Z" }, - { url = "https://files.pythonhosted.org/packages/ce/9e/f8288b034ab72b371513c13f9a41d9ba3effac54e24bfb467b007daee2ca/ruff-0.15.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:49fafa220220afe7758a487b048de4c8f9f767f37dfefad46b9dd06759d003eb", size = 10416177, upload-time = "2026-04-16T18:46:21.717Z" }, - { url = "https://files.pythonhosted.org/packages/85/71/504d79abfd3d92532ba6bbe3d1c19fada03e494332a59e37c7c2dabae427/ruff-0.15.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2ab8427e74a00d93b8bda1307b1e60970d40f304af38bccb218e056c220120d", size = 11221886, upload-time = "2026-04-16T18:46:15.086Z" }, - { url = "https://files.pythonhosted.org/packages/43/5a/947e6ab7a5ad603d65b474be15a4cbc6d29832db5d762cd142e4e3a74164/ruff-0.15.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:195072c0c8e1fc8f940652073df082e37a5d9cb43b4ab1e4d0566ab8977a13b7", size = 12075183, upload-time = "2026-04-16T18:46:07.944Z" }, - { url = "https://files.pythonhosted.org/packages/9f/a1/0b7bb6268775fdd3a0818aee8efd8f5b4e231d24dd4d528ced2534023182/ruff-0.15.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a0996d486af3920dec930a2e7daed4847dfc12649b537a9335585ada163e9e", size = 11516575, upload-time = "2026-04-16T18:46:31.687Z" }, - { url = "https://files.pythonhosted.org/packages/30/c3/bb5168fc4d233cc06e95f482770d0f3c87945a0cd9f614b90ea8dc2f2833/ruff-0.15.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bef2cb556d509259f1fe440bb9cd33c756222cf0a7afe90d15edf0866702431", size = 11306537, upload-time = "2026-04-16T18:46:36.988Z" }, - { url = "https://files.pythonhosted.org/packages/e4/92/4cfae6441f3967317946f3b788136eecf093729b94d6561f963ed810c82e/ruff-0.15.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:030d921a836d7d4a12cf6e8d984a88b66094ccb0e0f17ddd55067c331191bf19", size = 11296813, upload-time = "2026-04-16T18:46:24.182Z" }, - { url = "https://files.pythonhosted.org/packages/43/26/972784c5dde8313acde8ac71ba8ac65475b85db4a2352a76c9934361f9bc/ruff-0.15.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0e783b599b4577788dbbb66b9addcef87e9a8832f4ce0c19e34bf55543a2f890", size = 10633136, upload-time = "2026-04-16T18:46:39.802Z" }, - { url = "https://files.pythonhosted.org/packages/5b/53/3985a4f185020c2f367f2e08a103032e12564829742a1b417980ce1514a0/ruff-0.15.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ae90592246625ba4a34349d68ec28d4400d75182b71baa196ddb9f82db025ef5", size = 10424701, upload-time = "2026-04-16T18:46:10.381Z" }, - { url = "https://files.pythonhosted.org/packages/d3/57/bf0dfb32241b56c83bb663a826133da4bf17f682ba8c096973065f6e6a68/ruff-0.15.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1f111d62e3c983ed20e0ca2e800f8d77433a5b1161947df99a5c2a3fb60514f0", size = 10873887, upload-time = "2026-04-16T18:46:29.157Z" }, - { url = "https://files.pythonhosted.org/packages/02/05/e48076b2a57dc33ee8c7a957296f97c744ca891a8ffb4ffb1aaa3b3f517d/ruff-0.15.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:06f483d6646f59eaffba9ae30956370d3a886625f511a3108994000480621d1c", size = 11404316, upload-time = "2026-04-16T18:46:19.462Z" }, - { url = "https://files.pythonhosted.org/packages/88/27/0195d15fe7a897cbcba0904792c4b7c9fdd958456c3a17d2ea6093716a9a/ruff-0.15.11-py3-none-win32.whl", hash = "sha256:476a2aa56b7da0b73a3ee80b6b2f0e19cce544245479adde7baa65466664d5f3", size = 10655535, upload-time = "2026-04-16T18:46:12.47Z" }, - { url = "https://files.pythonhosted.org/packages/3a/5e/c927b325bd4c1d3620211a4b96f47864633199feed60fa936025ab27e090/ruff-0.15.11-py3-none-win_amd64.whl", hash = "sha256:8b6756d88d7e234fb0c98c91511aae3cd519d5e3ed271cae31b20f39cb2a12a3", size = 11779692, upload-time = "2026-04-16T18:46:17.268Z" }, - { url = "https://files.pythonhosted.org/packages/63/b6/aeadee5443e49baa2facd51131159fd6301cc4ccfc1541e4df7b021c37dd/ruff-0.15.11-py3-none-win_arm64.whl", hash = "sha256:063fed18cc1bbe0ee7393957284a6fe8b588c6a406a285af3ee3f46da2391ee4", size = 11032614, upload-time = "2026-04-16T18:46:34.487Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6e/e78ffb61d4686f3d96ba3df2c801161843746dcbcbb17a1e927d4829312b/ruff-0.15.12-py3-none-linux_armv6l.whl", hash = "sha256:f86f176e188e94d6bdbc09f09bfd9dc729059ad93d0e7390b5a73efe19f8861c", size = 10640713, upload-time = "2026-04-24T18:17:22.841Z" }, + { url = "https://files.pythonhosted.org/packages/ae/08/a317bc231fb9e7b93e4ef3089501e51922ff88d6936ce5cf870c4fe55419/ruff-0.15.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e3bcd123364c3770b8e1b7baaf343cc99a35f197c5c6e8af79015c666c423a6c", size = 11069267, upload-time = "2026-04-24T18:17:30.105Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fe87510d000220aa1ed530d4448a7c696a0cae1213e5ec30e5874287b66557b5", size = 10397182, upload-time = "2026-04-24T18:17:07.177Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/3310fc6d1b5e1fdea22bf3b1b807c7e187b581021b0d7d4514cccdb5fb71/ruff-0.15.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84a1630093121375a3e2a95b4a6dc7b59e2b4ee76216e32d81aae550a832d002", size = 10758012, upload-time = "2026-04-24T18:16:55.759Z" }, + { url = "https://files.pythonhosted.org/packages/11/c1/a606911aee04c324ddaa883ae418f3569792fd3c4a10c50e0dd0a2311e1e/ruff-0.15.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fb129f40f114f089ebe0ca56c0d251cf2061b17651d464bb6478dc01e69f11f5", size = 10447479, upload-time = "2026-04-24T18:16:51.677Z" }, + { url = "https://files.pythonhosted.org/packages/9d/68/4201e8444f0894f21ab4aeeaee68aa4f10b51613514a20d80bd628d57e88/ruff-0.15.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0c862b172d695db7598426b8af465e7e9ac00a3ea2a3630ee67eb82e366aaa6", size = 11234040, upload-time = "2026-04-24T18:17:16.529Z" }, + { url = "https://files.pythonhosted.org/packages/34/ff/8a6d6cf4ccc23fd67060874e832c18919d1557a0611ebef03fdb01fff11e/ruff-0.15.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2849ea9f3484c3aca43a82f484210370319e7170df4dfe4843395ddf6c57bc33", size = 12087377, upload-time = "2026-04-24T18:17:04.944Z" }, + { url = "https://files.pythonhosted.org/packages/85/f6/c669cf73f5152f623d34e69866a46d5e6185816b19fcd5b6dd8a2d299922/ruff-0.15.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e77c7e51c07fe396826d5969a5b846d9cd4c402535835fb6e21ce8b28fef847", size = 11367784, upload-time = "2026-04-24T18:17:25.409Z" }, + { url = "https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b2f4f2f3b1026b5fb449b467d9264bf22067b600f7b6f41fc5958909f449d0", size = 11344088, upload-time = "2026-04-24T18:17:12.258Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8d/49afab3645e31e12c590acb6d3b5b69d7aab5b81926dbaf7461f9441f37a/ruff-0.15.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9ba3b8f1afd7e2e43d8943e55f249e13f9682fde09711644a6e7290eb4f3e339", size = 11271770, upload-time = "2026-04-24T18:17:02.457Z" }, + { url = "https://files.pythonhosted.org/packages/46/06/33f41fe94403e2b755481cdfb9b7ef3e4e0ed031c4581124658d935d52b4/ruff-0.15.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e852ba9fdc890655e1d78f2df1499efbe0e54126bd405362154a75e2bde159c5", size = 10719355, upload-time = "2026-04-24T18:17:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/0d/59/18aa4e014debbf559670e4048e39260a85c7fcee84acfd761ac01e7b8d35/ruff-0.15.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dd8aed930da53780d22fc70bdf84452c843cf64f8cb4eb38984319c24c5cd5fd", size = 10462758, upload-time = "2026-04-24T18:17:32.347Z" }, + { url = "https://files.pythonhosted.org/packages/25/e7/cc9f16fd0f3b5fddcbd7ec3d6ae30c8f3fde1047f32a4093a98d633c6570/ruff-0.15.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:01da3988d225628b709493d7dc67c3b9b12c0210016b08690ef9bd27970b262b", size = 10953498, upload-time = "2026-04-24T18:17:20.674Z" }, + { url = "https://files.pythonhosted.org/packages/72/7a/a9ba7f98c7a575978698f4230c5e8cc54bbc761af34f560818f933dafa0c/ruff-0.15.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9cae0f92bd5700d1213188b31cd3bdd2b315361296d10b96b8e2337d3d11f53e", size = 11447765, upload-time = "2026-04-24T18:17:09.755Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f9/0ae446942c846b8266059ad8a30702a35afae55f5cdc54c5adf8d7afdc27/ruff-0.15.12-py3-none-win32.whl", hash = "sha256:d0185894e038d7043ba8fd6aee7499ece6462dc0ea9f1e260c7451807c714c20", size = 10657277, upload-time = "2026-04-24T18:17:18.591Z" }, + { url = "https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.whl", hash = "sha256:c87a162d61ab3adca47c03f7f717c68672edec7d1b5499e652331780fe74950d", size = 11837758, upload-time = "2026-04-24T18:17:00.113Z" }, + { url = "https://files.pythonhosted.org/packages/c0/98/6beb4b351e472e5f4c4613f7c35a5290b8be2497e183825310c4c3a3984b/ruff-0.15.12-py3-none-win_arm64.whl", hash = "sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f", size = 11120821, upload-time = "2026-04-24T18:16:57.979Z" }, ] [[package]] @@ -289,26 +386,26 @@ wheels = [ [[package]] name = "ty" -version = "0.0.32" +version = "0.0.34" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/85/7e/2aa791c9ae7b8cd5024cd4122e92267f664ca954cea3def3211919fa3c1f/ty-0.0.32.tar.gz", hash = "sha256:8743174c5f920f6700a4a0c9de140109189192ba16226884cd50095b43b8a45c", size = 5522294, upload-time = "2026-04-20T19:29:01.626Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/69/e24eefe2c35c0fdbdec9b60e162727af669bb76d64d993d982eb67b24c38/ty-0.0.34.tar.gz", hash = "sha256:a6efe66b0f13c03a65e6c72ec9abfe2792e2fd063c74fa67e2c4930e29d661be", size = 5585933, upload-time = "2026-05-01T23:06:46.388Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/eb/1075dc6a49d7acbe2584ae4d5b410c41b1f177a5adcc567e09eca4c69000/ty-0.0.32-py3-none-linux_armv6l.whl", hash = "sha256:dacbc2f6cd698d488ae7436838ff929570455bf94bfa4d9fe57a630c552aff83", size = 10902959, upload-time = "2026-04-20T19:28:31.907Z" }, - { url = "https://files.pythonhosted.org/packages/33/d2/c35fc8bc66e98d1ee9b0f8ed319bf743e450e1f1e997574b178fab75670f/ty-0.0.32-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:914bbc4f605ce2a9e2a78982e28fae1d3359a169d141f9dc3b4c7749cd5eca81", size = 10726172, upload-time = "2026-04-20T19:28:44.765Z" }, - { url = "https://files.pythonhosted.org/packages/96/32/c827da3ca480456fb02d8cea68a2609273b6c220fea0be9a4c8d8470b86e/ty-0.0.32-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4787ac9fe1f86b1f3133f5c6732adbe2df5668b50c679ac6e2d98cd284da812f", size = 10163701, upload-time = "2026-04-20T19:28:27.005Z" }, - { url = "https://files.pythonhosted.org/packages/ba/9e/2734478fbdb90c160cb2813a3916a16a2af5c1e231f87d635f6131d781fb/ty-0.0.32-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8ea0a728af99fe40dd744cba6441a2404f80b7f4bde17aa6da393810af5ea57", size = 10656220, upload-time = "2026-04-20T19:29:03.814Z" }, - { url = "https://files.pythonhosted.org/packages/44/9f/0007da2d35e424debe7e9f86ffbc1ab7f60983cfbc5f0411324ab2de5292/ty-0.0.32-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2850561f9b018ae33d7e5bbfa0ac414d3c518513edcffe43877dc9801446b9c5", size = 10696086, upload-time = "2026-04-20T19:28:46.829Z" }, - { url = "https://files.pythonhosted.org/packages/3b/5e/ce5fd4ec803222ae3e69a76d2a2db2eed55e19f5b131702b9789ef45f93d/ty-0.0.32-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b5fa2fb3c614349ee211d36476b49d88c5ef79a687cdb91b2872ad023b94d2f8", size = 11184800, upload-time = "2026-04-20T19:28:42.57Z" }, - { url = "https://files.pythonhosted.org/packages/6c/46/ebcf67a5999421331214aac51a7464db42de2be15bbe929c612a3ed0b039/ty-0.0.32-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b89969307ab2417d41c9be8059dd79feea577234e1e10d35132f5495e0d42c6", size = 11718718, upload-time = "2026-04-20T19:28:36.433Z" }, - { url = "https://files.pythonhosted.org/packages/18/2c/2141c86ed0ce0962b45cefb658a95e734f59759d47f20afdcd9c732910a1/ty-0.0.32-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b59868ede9b1d69a088f0d695df52a0061f95fa7baa1d5e0dc6fc9cf06e1334", size = 11346369, upload-time = "2026-04-20T19:28:48.967Z" }, - { url = "https://files.pythonhosted.org/packages/7a/da/ed6f772339cf29bd9a46def9d6db5084689eb574ee4d150ff704224c1ed8/ty-0.0.32-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8300caf35345498e9b9b03e550bba03cee8f5f5f8ab4c83c3b1ff1b7403b7d3a", size = 11280714, upload-time = "2026-04-20T19:28:51.516Z" }, - { url = "https://files.pythonhosted.org/packages/da/9b/c6813987edf4816a40e0c8e408b555f97d3f267c7b3a1688c8bbdf65609c/ty-0.0.32-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:583c7094f4574b02f724db924f98b804d1387a0bd9405ecb5e078cc0f47fbcfb", size = 10638806, upload-time = "2026-04-20T19:28:29.651Z" }, - { url = "https://files.pythonhosted.org/packages/4e/d4/0cefcbd2ad0f3d51762ccf58e652ec7da146eb6ae34f87228f6254bbb8be/ty-0.0.32-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e44ebe1bb4143a5628bc4db67ac0dfebe14594af671e4ee66f6f2e983da56501", size = 10726106, upload-time = "2026-04-20T19:29:06.3Z" }, - { url = "https://files.pythonhosted.org/packages/32/ad/2c8a97f91f06311f4367400f7d13534bbda2522c73c99a3e4c0757dff9b8/ty-0.0.32-py3-none-musllinux_1_2_i686.whl", hash = "sha256:06f17ada3e069cba6148342ef88e9929156beca8473e8d4f101b68f66c75643e", size = 10872951, upload-time = "2026-04-20T19:28:34.077Z" }, - { url = "https://files.pythonhosted.org/packages/ba/68/42293f9248106dd51875120971a5cc6ea315c2c4dcfb8e59aa063aa0af26/ty-0.0.32-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e96e60fa556cec04f15d7ea62d2ceee5982bd389233e961ab9fd42304e278175", size = 11363334, upload-time = "2026-04-20T19:28:54.036Z" }, - { url = "https://files.pythonhosted.org/packages/df/92/be9abf4d3e589ad5023e2ea965b93e204ec856420d46adf73c5c36c04678/ty-0.0.32-py3-none-win32.whl", hash = "sha256:2ff2ebb4986b24aebcf1444db7db5ca41b36086040e95eea9f8fb851c11e805c", size = 10260689, upload-time = "2026-04-20T19:28:56.541Z" }, - { url = "https://files.pythonhosted.org/packages/14/61/dc86acea899349d2579cb8419aecedd83dc504d7d6a10df65eef546c8300/ty-0.0.32-py3-none-win_amd64.whl", hash = "sha256:ba7284a4a954b598c1b31500352b3ec1f89bff533825592b5958848226fdc7ee", size = 11255371, upload-time = "2026-04-20T19:28:39.917Z" }, - { url = "https://files.pythonhosted.org/packages/43/01/beffec56d71ca25b343ede63adb076456b5b3e211f1c066452a44cd120b3/ty-0.0.32-py3-none-win_arm64.whl", hash = "sha256:7e10aadbdbda989a7d567ee6a37f8b98d4d542e31e3b190a2879fd581f75d658", size = 10658087, upload-time = "2026-04-20T19:28:59.286Z" }, + { url = "https://files.pythonhosted.org/packages/83/7b/8b85003d6639ef17a97dcbb31f4511cfe78f1c81a964470db100c8c883e7/ty-0.0.34-py3-none-linux_armv6l.whl", hash = "sha256:9ecc3d14f07a95a6ceb88e07f8e62358dbd37325d3d5bd56da7217ff1fef7fb8", size = 11067094, upload-time = "2026-05-01T23:06:21.133Z" }, + { url = "https://files.pythonhosted.org/packages/d7/25/b0098f65b020b015c40567c763fc66fffbec88b2ba6f584bca1e92f05ebb/ty-0.0.34-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0dccffd8a9d02321cd2dee3249df205e26d62694e741f4eeca36b157fd8b419f", size = 10840909, upload-time = "2026-05-01T23:06:18.409Z" }, + { url = "https://files.pythonhosted.org/packages/e4/55/5e4adcf7d2a1006b844903b27cb81244a9b748d850433a46a6c21776c401/ty-0.0.34-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b0ea47a2998e167ab3b21d2f4b5309a9cf33c297809f6d7e3e753252223174d0", size = 10279378, upload-time = "2026-05-01T23:06:37.962Z" }, + { url = "https://files.pythonhosted.org/packages/4d/91/f537dca0db8fe2558e8ab04d8941d687b384fcc1df5eb9023b2db75ac26c/ty-0.0.34-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b37da00b41a118a459ae56d8947e70651073fb33ebfbceb820e4a10b22d5023", size = 10817423, upload-time = "2026-05-01T23:06:26.247Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c4/55a3ad1da2815af1009bdc1b8c90dc11a364cd314e4b48c5128ba9d38859/ty-0.0.34-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:81cbbb93c2342fe3de43e625d3a9eb149633e9f485e816ebf6395d08685355d8", size = 10851826, upload-time = "2026-05-01T23:06:24.198Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/9c7606af22d73fb43ea4369472d9c66ece11231be73b0efe8e3c61655559/ty-0.0.34-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c5b4dea1594a021289e172582df9cde7089dce14b276fc650e7b212b1772e12", size = 11356318, upload-time = "2026-05-01T23:06:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/20/54/bb423f663721ab4138b216425c6b55eaefd3a068243b24d6d8fe988f4e13/ty-0.0.34-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:030fb00aa2d2a5b5ae9d9183d574e0c82dae80566700a7490c43669d8ece40cd", size = 11902968, upload-time = "2026-05-01T23:06:35.82Z" }, + { url = "https://files.pythonhosted.org/packages/b6/22/01122b21ab6b534a2f618c6bbe5f1f7f49fd56f4b2ec8887cd6d40d08fb3/ty-0.0.34-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ae9555e24e36c63a8218e037a5a63f15579eb6aa94f41017e57cd41d335cfb5", size = 11548860, upload-time = "2026-05-01T23:06:42.155Z" }, + { url = "https://files.pythonhosted.org/packages/d1/50/86008b1392ec64bed1957bbcc7aaa43b466b50dfc91bb131841c21d7c5c3/ty-0.0.34-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99eb23df9ed129fc26d1ab00d6f0b8dfe5253b09c2ac6abdb11523fa70d67f10", size = 11457097, upload-time = "2026-05-01T23:06:53.477Z" }, + { url = "https://files.pythonhosted.org/packages/92/3e/4558b2296963ba99c58d8409c57d7db4f3061b656c3613cb21c02c1ef4c2/ty-0.0.34-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:85de45382016eceae69e104815eb2cfa200787df104002e262a86cbd43ed2c02", size = 10798192, upload-time = "2026-05-01T23:06:40.004Z" }, + { url = "https://files.pythonhosted.org/packages/76/bf/650d24402be2ef678528d60caac1d9477a40fc37e3792ecef07834fd7a4a/ty-0.0.34-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:14cb575fb8fa5131f5129d100cfe23c1575d23faf5dfc5158432749a3e38c9b5", size = 10890390, upload-time = "2026-05-01T23:06:33.076Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ef/ccd2ca13906079f7935fd7e067661b24233017f57d987d51d6a121d85bb5/ty-0.0.34-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c6fc0b69d8450e6910ba9db34572b959b81329a97ae273c391f70e9fb6c1aade", size = 11031564, upload-time = "2026-05-01T23:06:55.812Z" }, + { url = "https://files.pythonhosted.org/packages/ba/2d/d27b72005b6f43599e3bcabab0d7135ac0c230b7a307bb99f9eea02c1cda/ty-0.0.34-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:30dfcec2f0fde3993f4f912ed0e057dcbebc8615299f610a4c2ddb7b5a3e1e06", size = 11553430, upload-time = "2026-05-01T23:06:31.096Z" }, + { url = "https://files.pythonhosted.org/packages/a7/12/20812e1ad930b8d4af70eebf19ad23cff6e31efcfa613ef884531fcdbaa1/ty-0.0.34-py3-none-win32.whl", hash = "sha256:97b77ddf007271b812a313a8f0a14929bc5590958433e1fb83ef585676f53342", size = 10436048, upload-time = "2026-05-01T23:06:49.108Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/afa095c5987868fbda27c0f731146ac8e3d07b357adfa83daccaee5b1a16/ty-0.0.34-py3-none-win_amd64.whl", hash = "sha256:1f543968accb952705134028d1fda8656882787dbbc667ad4d6c3ba23791d604", size = 11462526, upload-time = "2026-05-01T23:06:28.514Z" }, + { url = "https://files.pythonhosted.org/packages/63/8f/bf041a06260d77662c0605e56dacfe90b786bf824cbe1aed238d15fe5e84/ty-0.0.34-py3-none-win_arm64.whl", hash = "sha256:ea09108cbcb16b6b06d7596312b433bf49681e78d30e4dc7fb3c1b248a95e09a", size = 10846945, upload-time = "2026-05-01T23:06:44.428Z" }, ] [[package]] @@ -335,6 +432,7 @@ source = { editable = "." } dependencies = [ { name = "click" }, { name = "deepmerge" }, + { name = "jinja2" }, { name = "markdown" }, { name = "pygments" }, { name = "pymdown-extensions" }, @@ -355,6 +453,7 @@ dev = [ requires-dist = [ { name = "click", specifier = ">=8.1.8" }, { name = "deepmerge", specifier = ">=2.0" }, + { name = "jinja2", specifier = ">=3.1" }, { name = "markdown", specifier = ">=3.7" }, { name = "pygments", specifier = ">=2.20" }, { name = "pymdown-extensions", specifier = ">=10.21.2" }, @@ -365,7 +464,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ { name = "maturin", specifier = ">=1.10.2" }, - { name = "pytest", specifier = ">=8.0" }, + { name = "pytest", specifier = ">=9.0.3" }, { name = "ruff", specifier = ">=0.12.8" }, { name = "ty", specifier = ">=0.0.32" }, { name = "types-pyyaml", specifier = ">=6.0.12" },