From 7ce799e63758bdfb5461a9461cbc15855edaf57f Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Tue, 25 Nov 2025 12:10:52 +0100 Subject: [PATCH 01/26] adding a tests framework --- pyproject.toml | 145 +++++++++ sphinx_simplepdf/builders/debug.py | 10 +- sphinx_simplepdf/builders/simplepdf.py | 11 +- tests/__init__.py | 1 + tests/conftest.py | 279 ++++++++++++++++++ tests/doc_test/basic_doc/conf.py | 8 + tests/doc_test/basic_doc/content.rst | 17 ++ tests/doc_test/basic_doc/index.rst | 22 ++ tests/doc_test/with_images/_static/.gitkeep | 2 + tests/doc_test/with_images/conf.py | 7 + tests/doc_test/with_images/index.rst | 18 ++ tests/doc_test/with_issues/broken_anchors.rst | 14 + tests/doc_test/with_issues/conf.py | 7 + tests/doc_test/with_issues/index.rst | 14 + tests/doc_test/with_toc/chapter1.rst | 17 ++ tests/doc_test/with_toc/chapter2.rst | 17 ++ tests/doc_test/with_toc/conf.py | 8 + tests/doc_test/with_toc/index.rst | 19 ++ tests/test_basic_build.py | 58 ++++ tests/test_html_processing.py | 54 ++++ tests/test_pdf_generation.py | 89 ++++++ tests/test_theme_features.py | 44 +++ tests/test_warnings.py | 62 ++++ tests/utils.py | 14 + 24 files changed, 929 insertions(+), 8 deletions(-) create mode 100644 pyproject.toml create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/doc_test/basic_doc/conf.py create mode 100644 tests/doc_test/basic_doc/content.rst create mode 100644 tests/doc_test/basic_doc/index.rst create mode 100644 tests/doc_test/with_images/_static/.gitkeep create mode 100644 tests/doc_test/with_images/conf.py create mode 100644 tests/doc_test/with_images/index.rst create mode 100644 tests/doc_test/with_issues/broken_anchors.rst create mode 100644 tests/doc_test/with_issues/conf.py create mode 100644 tests/doc_test/with_issues/index.rst create mode 100644 tests/doc_test/with_toc/chapter1.rst create mode 100644 tests/doc_test/with_toc/chapter2.rst create mode 100644 tests/doc_test/with_toc/conf.py create mode 100644 tests/doc_test/with_toc/index.rst create mode 100644 tests/test_basic_build.py create mode 100644 tests/test_html_processing.py create mode 100644 tests/test_pdf_generation.py create mode 100644 tests/test_theme_features.py create mode 100644 tests/test_warnings.py create mode 100644 tests/utils.py diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..398da0c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,145 @@ +[build-system] +requires = ["flit_core >=3.4,<4"] +build-backend = "flit_core.buildapi" + +[project] +name = "sphinx-simplepdf" +version = "1.6.0" +description = "An easy to use PDF Builder for Sphinx with a modern PDF-Theme." +readme = "README.rst" +license = { text = "MIT" } +requires-python = ">=3.10" +authors = [{ name = "team useblocks", email = "info@useblocks.com" }] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "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", + "Topic :: Documentation", + "Topic :: Utilities", + "Framework :: Sphinx :: Extension", +] +dependencies = [ + "sphinx>=4.0", + "weasyprint>=66.0", + "libsass", + "beautifulsoup4", + "packaging>=20.0", # Modern replacement for pkg_resources.parse_version + "fonttools>=4.0.0", + "pillow>=9.1.0", + "pydyf>=0.10.0", + "cffi>=0.6", + "pycairo", +] + +[project.entry-points."sphinx.html_themes"] +simplepdf_theme = "sphinx_simplepdf.themes.simplepdf_theme" + +[project.entry-points."sphinx.builders"] +simplepdf = "sphinx_simplepdf.builders.simplepdf" + + +[project.optional-dependencies] +dev = [ + "pre-commit>=3.5", + "pytest>=7.0", + "pytest-xdist>=3.0", + "tox>=4.0", + "tox-uv>=1.0", + "ruff>=0.4.0", + "pdfminer.six" +] +docs = [ + "sphinx>=4.0", + "sphinxcontrib-plantuml", + "sphinx-needs", + "sphinx-copybutton", +] + +[project.urls] +Homepage = "https://github.com/useblocks/sphinx-simplepdf" +Documentation = "https://sphinx-simplepdf.readthedocs.io/en/latest/" +Repository = "https://github.com/useblocks/sphinx-simplepdf" +"Bug Tracker" = "https://github.com/useblocks/sphinx-simplepdf/issues" + +[tool.ruff] +line-length = 120 +target-version = "py310" + +[tool.ruff.lint] +extend-select = [ + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "F", # pyflakes (includes unused imports) + "I", # isort + "ICN", # flake8-import-conventions + "ISC", # flake8-implicit-str-concat + "N", # pep8-naming + "RUF", # Ruff-specific rules + "SIM", # flake8-simplify + "UP", # pyupgrade +] +extend-ignore = ["B904", "ISC001", "ICN001", "N818", "RUF012"] + +[tool.ruff.lint.isort] +known-first-party = ["sphinx_simplepdf"] +force-sort-within-sections = true + +[tool.tox] +# To use tox, see https://tox.readthedocs.io +# $ pipx install tox +# it is suggested to use with https://github.com/tox-dev/tox-uv or https://github.com/tox-dev/tox-conda +# $ pipx inject tox tox-uv + +legacy_tox_ini = """ +[tox] +env_list = + py{310,311,312,313}-sphinx{74,82} + docs + lint +requires = + tox>=4 + tox-uv>=1.0 + +[testenv] +deps = + sphinx>=4.0 + pytest + pdfminer.six + sphinx-simplepdf @ . +commands = + pytest tests/ + +[testenv:lint] +description = Run code quality checks +commands = + ruff check sphinx_simplepdf/ + ruff format --check sphinx_simplepdf/ + +[testenv:docs] +description = Build documentation with Sphinx +basepython = python3.12 +deps = + sphinx>=7.0 +extras = docs +commands = + sphinx-build -a -E -j auto -b html docs/ docs/_build +""" + +# [tool.setuptools.packages.find] +# where = ["sphinx_simplepdf/themes/simplepdf_theme"] + +# [tool.setuptools.package-data] +# sphinx_simplepdf=[ +# 'theme.conf', +# '*.html', +# 'static/styles/*.css', +# 'static/js/*.js', +# 'static/fonts/*.*' +# ] diff --git a/sphinx_simplepdf/builders/debug.py b/sphinx_simplepdf/builders/debug.py index f505082..a27ac40 100644 --- a/sphinx_simplepdf/builders/debug.py +++ b/sphinx_simplepdf/builders/debug.py @@ -1,7 +1,11 @@ import sys import pkgutil -import pkg_resources import platform +try: + from importlib.metadata import version +except ImportError: + from importlib_metadata import version + class DebugPython: @@ -17,11 +21,11 @@ def get_packages(self): final = {} for name in names: try: - version = pkg_resources.get_distribution(name).version + __version__ = version(name) except (Exception): final[name] = 'unknown' else: - final[name] = version + final[name] = __version__ return final diff --git a/sphinx_simplepdf/builders/simplepdf.py b/sphinx_simplepdf/builders/simplepdf.py index 25b6518..a40478b 100644 --- a/sphinx_simplepdf/builders/simplepdf.py +++ b/sphinx_simplepdf/builders/simplepdf.py @@ -181,9 +181,9 @@ def finish(self) -> None: success = True break except subprocess.TimeoutExpired: - logger.warning(f"TimeoutExpired in weasyprint, retrying") + logger.info(f"TimeoutExpired in weasyprint, retrying") except subprocess.CalledProcessError as e: - logger.warning( + logger.info( f"CalledProcessError in weasyprint, retrying\n{str(e)}" ) finally: @@ -218,7 +218,7 @@ def finish(self) -> None: """ def _toctree_fix(self, html): - print("checking for potential toctree page numbering errors") + logger.info("checking for potential toctree page numbering errors") soup = BeautifulSoup(html, "html.parser") sidebar = soup.find("div", class_="sphinxsidebarwrapper") @@ -251,8 +251,7 @@ def _toctree_fix(self, html): references = {key: value for key, value in counts.items()} if references: - - print(f"found duplicate chapters:\n{references}") + logger.info(f"found duplicate chapters:\n{references}") for text in references.keys(): @@ -356,7 +355,9 @@ def _toctree_fix(self, html): heading.attrs["class"] = class_attr + logger.debug("DEBUG HTML START") logger.debug(soup.prettify(formatter="html")) + logger.debug("DEBUG HTML END") return str(soup) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..3f5cbb5 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Test package for sphinx-simplepdf.""" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..49d5b1b --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,279 @@ +"""Central pytest configuration and fixtures for sphinx-simplepdf tests.""" +from pathlib import Path +from typing import Any +import shutil + +import pytest +from sphinx.application import Sphinx +from sphinx.testing.util import SphinxTestApp + + +pytest_plugins = ("sphinx.testing.fixtures",) + +def copy_srcdir_to_tmpdir(srcdir: Path, tmp: Path) -> Path: + """ + Copy Source Directory to Temporary Directory. + + This function copies the contents of a source directory to a temporary + directory. It generates a random subdirectory within the temporary directory + to avoid conflicts and enable parallel processes to run without conflicts. + + :param srcdir: Path to the source directory. + :param tmp: Path to the temporary directory. + + :return: Path to the newly created directory in the temporary directory. + """ + srcdir = Path(__file__).parent.absolute() / srcdir + tmproot = tmp / Path(srcdir).name + shutil.copytree(srcdir, tmproot) + return tmproot + + +@pytest.fixture(scope="session") +def rootdir(): + """Root directory for test documentation projects.""" + return Path(__file__).parent / "doc_test" + + +@pytest.fixture +def content(request): + """ + Provide test document content dynamically. + + Usage in test: + @pytest.mark.parametrize('content', [ + {'index.rst': '.. title::\n\n Test\n'} + ], indirect=True) + def test_example(app): + ... + """ + return request.param if hasattr(request, "param") else {} + + +class SphinxBuild: + """Helper class to build Sphinx documentation and access results.""" + + def __init__(self, app: Sphinx, src: Path): + self.app = app + self.src = src + self.outdir = None + self.warnings = [] + self.errors = [] + + def build(self, force_all: bool = True, raise_on_warning: bool = False): + """ + Build the documentation. + + Args: + force_all: Rebuild all files + raise_on_warning: Raise exception on warnings + """ + self.app.build(force_all=force_all) + self.outdir = Path(self.app.builder.outdir) + + # Collect warnings and errors from status/warning streams + warning_content = self.app._warning.getvalue() + if warning_content: + self.warnings = [ + line.strip() + for line in warning_content.split("\n") + if line.strip() + ] + + if raise_on_warning and self.warnings: + raise AssertionError( + f"Build produced warnings:\n" + "\n".join(self.warnings) + ) + + return self + + def html_content(self, docname: str = "index") -> str: + """ + Get generated HTML content (before SimplePDF processing). + + Args: + docname: Document name without extension + + Returns: + HTML content as string + """ + html_file = self.outdir / f"{docname}.html" + if not html_file.exists(): + raise FileNotFoundError(f"HTML file not found: {html_file}") + return html_file.read_text(encoding="utf-8") + + def processed_html(self) -> str: + """ + Get processed HTML from SimplePDF builder (from debug output). + + The SimplePDF builder logs the processed HTML before PDF generation. + This extracts it from the warning stream. + + Returns: + Processed HTML content as string + """ + # Look for debug HTML output in warnings + # (SimplePDF should log processed HTML with specific marker) + for i, line in enumerate(self.warnings): + if "DEBUG HTML START" in line: + # Find end marker + html_lines = [] + for next_line in self.warnings[i+1:]: + if "DEBUG HTML END" in next_line: + break + html_lines.append(next_line) + return "\n".join(html_lines) + + raise ValueError("No processed HTML found in debug output") + + def pdf_path(self, basename: str = None) -> Path: + """ + Get path to generated PDF file. + + Args: + basename: PDF filename without extension (default: project name) + + Returns: + Path to PDF file + """ + if basename is None: + basename = self.app.config.project + + pdf_file = self.outdir / f"{basename}.pdf" + if not pdf_file.exists(): + raise FileNotFoundError(f"PDF file not found: {pdf_file}") + return pdf_file + + def pdf_exists(self, basename: str = None) -> bool: + """Check if PDF was generated.""" + try: + self.pdf_path(basename) + return True + except FileNotFoundError: + return False + + def has_warnings(self, pattern: str = None) -> bool: + """ + Check if build produced warnings. + + Args: + pattern: Optional regex pattern to match specific warnings + + Returns: + True if warnings exist (and match pattern if provided) + """ + if pattern is None: + return len(self.warnings) > 0 + + import re + regex = re.compile(pattern) + return any(regex.search(w) for w in self.warnings) + + def get_warnings_matching(self, pattern: str) -> list[str]: + """ + Get all warnings matching a pattern. + + Args: + pattern: Regex pattern to match + + Returns: + List of matching warning messages + """ + import re + regex = re.compile(pattern) + return [w for w in self.warnings if regex.search(w)] + + +@pytest.fixture +def sphinx_build(make_app, tmp_path): + """ + Main fixture to build Sphinx projects with SimplePDF. + + Usage: + def test_example(sphinx_build): + app = sphinx_build( + buildername='simplepdf', + srcdir='basic_doc', + confoverrides={'project': 'Test'} + ) + result = app.build() + assert result.pdf_exists() + """ + def _make_build( + buildername: str = "simplepdf", + srcdir: str = None, + confoverrides: dict[str, Any] = None, + **kwargs + ) -> SphinxBuild: + """ + Create and return SphinxBuild instance. + + Args: + buildername: Sphinx builder name + srcdir: Source directory name (relative to doc_test/) + confoverrides: Dictionary of conf.py overrides + **kwargs: Additional arguments for make_app + + Returns: + SphinxBuild instance + """ + if srcdir is None: + raise ValueError("srcdir is required") + + # Resolve source directory using pathlib + test_root = Path(__file__).parent / "doc_test" + src_path = test_root / srcdir + src_dir = copy_srcdir_to_tmpdir(f"doc_test/{srcdir}", tmp_path) + + if not src_path.exists(): + raise ValueError(f"Test document directory not found: {src_path}") + + print(f"using src_dir {src_dir}") + + # Create app with Path object (Sphinx 8.2 compatible) + app = make_app( + buildername=buildername, + # srcdir=src_path, + srcdir=src_dir, + confoverrides=confoverrides or {}, + **kwargs + ) + + return SphinxBuild(app, src_path) + + return _make_build + + +@pytest.fixture +def sphinx_build_factory(tmp_path, rootdir): + """ + Factory fixture to create multiple test builds. + + More flexible than sphinx_build for tests that need multiple builds. + """ + def _factory( + srcdir: str, + buildername: str = "simplepdf", + **kwargs + ): + src = rootdir / srcdir + app = SphinxTestApp( + buildername=buildername, + srcdir=src, + freshenv=True, + **kwargs + ) + return SphinxBuild(app, src) + + return _factory + + +@pytest.fixture +def minimal_conf(): + """Minimal conf.py configuration for SimplePDF.""" + return { + "extensions": ["sphinx_simplepdf"], + "simplepdf_theme": "simplepdf_theme", + "master_doc": "index", + "exclude_patterns": ["_build"], + } diff --git a/tests/doc_test/basic_doc/conf.py b/tests/doc_test/basic_doc/conf.py new file mode 100644 index 0000000..ebe3e0a --- /dev/null +++ b/tests/doc_test/basic_doc/conf.py @@ -0,0 +1,8 @@ +"""Minimal test configuration.""" +project = "BasicTest" +extensions = ["sphinx_simplepdf"] +master_doc = "index" +exclude_patterns = ["_build"] + +# SimplePDF settings +simplepdf_theme = "simplepdf_theme" diff --git a/tests/doc_test/basic_doc/content.rst b/tests/doc_test/basic_doc/content.rst new file mode 100644 index 0000000..9ef8114 --- /dev/null +++ b/tests/doc_test/basic_doc/content.rst @@ -0,0 +1,17 @@ +Additional Content +================== + +This is additional content for testing. + +Lists +----- + +* Item 1 +* Item 2 +* Item 3 + +Numbered list: + +1. First +2. Second +3. Third diff --git a/tests/doc_test/basic_doc/index.rst b/tests/doc_test/basic_doc/index.rst new file mode 100644 index 0000000..7d3c3f9 --- /dev/null +++ b/tests/doc_test/basic_doc/index.rst @@ -0,0 +1,22 @@ +Basic Test Document +=================== + +This is a minimal test document for SimplePDF. + +Section 1 +--------- + +Some content in section 1. + +Section 2 +--------- + +Some content in section 2. + +Code Example +------------ + +.. code-block:: python + + def hello_world(): + print("Hello, World!") diff --git a/tests/doc_test/with_images/_static/.gitkeep b/tests/doc_test/with_images/_static/.gitkeep new file mode 100644 index 0000000..5f9ec71 --- /dev/null +++ b/tests/doc_test/with_images/_static/.gitkeep @@ -0,0 +1,2 @@ +# Placeholder for static files directory +# Add test_image.png here for actual tests diff --git a/tests/doc_test/with_images/conf.py b/tests/doc_test/with_images/conf.py new file mode 100644 index 0000000..81587dc --- /dev/null +++ b/tests/doc_test/with_images/conf.py @@ -0,0 +1,7 @@ +"""Configuration with images.""" +project = "ImagesTest" +extensions = ["sphinx_simplepdf"] +master_doc = "index" +exclude_patterns = ["_build"] + +simplepdf_theme = "simplepdf_theme" diff --git a/tests/doc_test/with_images/index.rst b/tests/doc_test/with_images/index.rst new file mode 100644 index 0000000..25403ff --- /dev/null +++ b/tests/doc_test/with_images/index.rst @@ -0,0 +1,18 @@ +Document with Images +==================== + +This document contains images for testing. + +Test Image +---------- + +.. image:: _static/test_image.png + :alt: Test image + :width: 200px + +The image above should be included in the PDF. + +Another Section +--------------- + +More content after the image. diff --git a/tests/doc_test/with_issues/broken_anchors.rst b/tests/doc_test/with_issues/broken_anchors.rst new file mode 100644 index 0000000..ee37d8c --- /dev/null +++ b/tests/doc_test/with_issues/broken_anchors.rst @@ -0,0 +1,14 @@ +Broken Anchors +============== + +This page contains broken internal references. + +Section with Broken Links +-------------------------- + +This is a reference to a `non-existent section <#does-not-exist>`_. + +Another broken reference: see :ref:`missing-label`. + +.. note:: + This note contains a broken link to :doc:`missing_document`. diff --git a/tests/doc_test/with_issues/conf.py b/tests/doc_test/with_issues/conf.py new file mode 100644 index 0000000..502eef7 --- /dev/null +++ b/tests/doc_test/with_issues/conf.py @@ -0,0 +1,7 @@ +"""Configuration for testing error/warning handling.""" +project = "IssuesTest" +extensions = ["sphinx_simplepdf"] +master_doc = "index" +exclude_patterns = ["_build"] + +simplepdf_theme = "simplepdf_theme" diff --git a/tests/doc_test/with_issues/index.rst b/tests/doc_test/with_issues/index.rst new file mode 100644 index 0000000..cf04aea --- /dev/null +++ b/tests/doc_test/with_issues/index.rst @@ -0,0 +1,14 @@ +Document with Issues +==================== + +This document contains intentional issues for testing warning handling. + +.. toctree:: + :maxdepth: 2 + + broken_anchors + +Main Content +------------ + +This document has broken references for testing. diff --git a/tests/doc_test/with_toc/chapter1.rst b/tests/doc_test/with_toc/chapter1.rst new file mode 100644 index 0000000..7c08e48 --- /dev/null +++ b/tests/doc_test/with_toc/chapter1.rst @@ -0,0 +1,17 @@ +Chapter 1: Getting Started +========================== + +This is the first chapter of the test document. + +Section 1.1 +----------- + +Content for section 1.1. + +Section 1.2 +----------- + +Content for section 1.2. + +.. note:: + This is a note in chapter 1. diff --git a/tests/doc_test/with_toc/chapter2.rst b/tests/doc_test/with_toc/chapter2.rst new file mode 100644 index 0000000..0d6d339 --- /dev/null +++ b/tests/doc_test/with_toc/chapter2.rst @@ -0,0 +1,17 @@ +Chapter 2: Advanced Topics +========================== + +This is the second chapter. + +Section 2.1 +----------- + +Advanced content here. + +Section 2.2 +----------- + +More advanced topics. + +.. warning:: + This is a warning in chapter 2. diff --git a/tests/doc_test/with_toc/conf.py b/tests/doc_test/with_toc/conf.py new file mode 100644 index 0000000..29bfc77 --- /dev/null +++ b/tests/doc_test/with_toc/conf.py @@ -0,0 +1,8 @@ +"""Configuration with table of contents.""" +project = "TOCTest" +extensions = ["sphinx_simplepdf"] +master_doc = "index" +exclude_patterns = ["_build"] + +simplepdf_theme = "simplepdf_theme" +simplepdf_toc = True diff --git a/tests/doc_test/with_toc/index.rst b/tests/doc_test/with_toc/index.rst new file mode 100644 index 0000000..0e3d3cf --- /dev/null +++ b/tests/doc_test/with_toc/index.rst @@ -0,0 +1,19 @@ +Document with TOC +================= + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + chapter1 + chapter2 + +Main Content +------------ + +This document has a table of contents that links to multiple chapters. + +Introduction +~~~~~~~~~~~~ + +This is the introduction section. diff --git a/tests/test_basic_build.py b/tests/test_basic_build.py new file mode 100644 index 0000000..ceebf49 --- /dev/null +++ b/tests/test_basic_build.py @@ -0,0 +1,58 @@ +"""Basic build tests for SimplePDF.""" +import pytest +from .utils import build_and_capture_stdout + + +def test_basic_build_succeeds(sphinx_build, capsys): + """Test that a basic document builds successfully.""" + result = build_and_capture_stdout(sphinx_build, capsys, srcdir="basic_doc") + + assert result.pdf_exists() + # assert not result.has_warnings() + assert not result.has_warnings("ERROR:") + + +def test_html_generation(sphinx_build, capsys): + """Test that HTML is generated before PDF conversion.""" + result = build_and_capture_stdout(sphinx_build, capsys, srcdir="basic_doc") + + html = result.html_content("index") + assert "" in html + + +def test_build_with_custom_project_name(sphinx_build, capsys): + """Test that custom project name is used for PDF filename.""" + project_name = "MyCustomProject" + result = build_and_capture_stdout(sphinx_build, capsys, + srcdir="basic_doc", + confoverrides={"project": project_name} + ) + + assert result.pdf_exists(project_name) + assert not result.has_warnings("ERROR:") + + + +def test_rebuild_does_not_fail(sphinx_build): + """Test that rebuilding does not cause errors.""" + builder = sphinx_build(srcdir="basic_doc") + + # First build + result1 = builder.build() + assert result1.pdf_exists() + + # Second build + result2 = builder.build() + assert result2.pdf_exists() + + +@pytest.mark.parametrize("srcdir", [ + "basic_doc", + "with_images", + "with_toc", +]) +def test_various_document_types(sphinx_build, capsys, srcdir): + """Test that various document types build successfully.""" + result = build_and_capture_stdout(sphinx_build, capsys, srcdir=srcdir) + assert result.pdf_exists() diff --git a/tests/test_html_processing.py b/tests/test_html_processing.py new file mode 100644 index 0000000..bb78004 --- /dev/null +++ b/tests/test_html_processing.py @@ -0,0 +1,54 @@ +"""Tests for HTML processing in SimplePDF builder.""" +import pytest +import re + + +def test_html_is_processed(sphinx_build): + """Test that HTML is processed by SimplePDF builder.""" + result = sphinx_build(srcdir="basic_doc").build() + + # Original HTML should exist + original_html = result.html_content("index") + assert original_html + + # Processed HTML should be in debug output + # (requires SimplePDF to log processed HTML) + try: + processed_html = result.processed_html() + assert processed_html + # Processed HTML should differ from original + assert processed_html != original_html + except ValueError: + pytest.skip("SimplePDF debug output not available") + + +def test_anchors_are_preserved(sphinx_build): + """Test that anchors/IDs are preserved in HTML processing.""" + result = sphinx_build(srcdir="with_toc").build() + + html = result.html_content("index") + + # Check for section anchors + assert 'id="' in html or 'href="#' in html + + +def test_image_paths_are_resolved(sphinx_build): + """Test that image paths are correctly resolved.""" + result = sphinx_build(srcdir="with_images").build() + + html = result.html_content("index") + + # Images should be referenced + assert " 0 + + +def test_pdf_contains_content(sphinx_build, capsys): + """Test that PDF contains actual content (not empty).""" + result = build_and_capture_stdout(sphinx_build, capsys, srcdir="basic_doc") + + pdf_path = result.pdf_path() + # PDF should be reasonably sized (> 1KB) + assert pdf_path.stat().st_size > 1024 + + +def test_pdf_with_images(sphinx_build, capsys): + """Test that PDF with images is generated.""" + result = build_and_capture_stdout(sphinx_build, capsys, srcdir="with_images") + + # Should build without errors + assert result.pdf_exists() + # PDF with images should be larger + pdf_path = result.pdf_path() + assert pdf_path.stat().st_size > 5000 + + +def test_pdf_with_toc(sphinx_build, capsys): + """Test that PDF with table of contents is generated.""" + # result = sphinx_build(srcdir="with_toc").build() + result = build_and_capture_stdout(sphinx_build, capsys, srcdir="with_toc") + + assert result.pdf_exists() + # TOC usually makes PDF larger + pdf_path = result.pdf_path() + assert pdf_path.stat().st_size > 2000 + + # Check for specific WeasyPrint anchor warnings + anchor_warnings = result.get_warnings_matching(r"(anchor|link|reference)") + assert len(anchor_warnings) == 0 + + text = extract_pdf_text(pdf_path) + + assert """ +Table of Contents + +Contents: + +Chapter 1: Getting Started + +• Section 1.1 +• Section 1.2 + +Chapter 2: Advanced Topics + +• Section 2.1 +• Section 2.2 + +4 + +4 + +4 + +5 + +5 + +5 +""" in text + + +@pytest.mark.parametrize("page_format", ["A4", "Letter", "A5"]) +def test_pdf_different_page_formats(sphinx_build, capsys, page_format): + """Test PDF generation with different page formats.""" + result = build_and_capture_stdout(sphinx_build, capsys, + srcdir="basic_doc", + confoverrides={"simplepdf_page_size": page_format} + ) + + assert result.pdf_exists() diff --git a/tests/test_theme_features.py b/tests/test_theme_features.py new file mode 100644 index 0000000..e117151 --- /dev/null +++ b/tests/test_theme_features.py @@ -0,0 +1,44 @@ +"""Tests for SimplePDF theme features.""" +import pytest + + +def test_default_theme_applied(sphinx_build): + """Test that default SimplePDF theme is applied.""" + result = sphinx_build(srcdir="basic_doc").build() + + assert result.pdf_exists() + + +def test_custom_theme_settings(sphinx_build): + """Test that custom theme settings are applied.""" + result = sphinx_build( + srcdir="basic_doc", + confoverrides={ + "simplepdf_theme_options": { + "primary_color": "#FF0000", + } + } + ).build() + + assert result.pdf_exists() + + +def test_toc_generation(sphinx_build): + """Test that table of contents is generated.""" + result = sphinx_build( + srcdir="with_toc", + confoverrides={"simplepdf_toc": True} + ).build() + + assert result.pdf_exists() + + +@pytest.mark.parametrize("font_size", ["10pt", "12pt", "14pt"]) +def test_different_font_sizes(sphinx_build, font_size): + """Test PDF generation with different font sizes.""" + result = sphinx_build( + srcdir="basic_doc", + confoverrides={"simplepdf_font_size": font_size} + ).build() + + assert result.pdf_exists() diff --git a/tests/test_warnings.py b/tests/test_warnings.py new file mode 100644 index 0000000..c90333e --- /dev/null +++ b/tests/test_warnings.py @@ -0,0 +1,62 @@ +"""Tests for warnings and error handling.""" +import pytest +import re + +from .utils import build_and_capture_stdout + + +def test_broken_anchors_warning(sphinx_build, capsys): + """Test that broken anchors produce warnings.""" + result = build_and_capture_stdout(sphinx_build, capsys, srcdir="with_issues") + + # Should have warnings about broken links/anchors + assert result.has_warnings() + + # Check for specific WeasyPrint anchor warnings + anchor_warnings = result.get_warnings_matching(r"(anchor|link|reference)") + assert len(anchor_warnings) > 0 + + +def test_missing_image_warning(sphinx_build, tmp_path): + """Test that missing images produce warnings.""" + # This would require a test doc with broken image reference + pytest.skip("Requires test doc with broken image") + + +def test_build_warnings_are_captured(sphinx_build, capsys): + """Test that all build warnings are captured.""" + result = build_and_capture_stdout(sphinx_build, capsys, srcdir="with_issues") + + # Warnings should be accessible + assert isinstance(result.warnings, list) + + +def test_weasyprint_warnings_logged(sphinx_build, capsys): + """Test that WeasyPrint warnings are logged.""" + result = build_and_capture_stdout(sphinx_build, capsys, srcdir="with_issues") + + # Look for WeasyPrint-specific warnings + weasy_warnings = result.get_warnings_matching(r"(weasyprint|WeasyPrint)") + # May or may not have WeasyPrint warnings depending on document + assert isinstance(weasy_warnings, list) + + +@pytest.mark.parametrize("strict_mode", [True, False]) +def test_strict_mode_handling(sphinx_build, capsys, strict_mode): + """Test behavior with strict mode enabled/disabled.""" + confoverrides = {"nitpicky": strict_mode} + + if strict_mode: + # In strict mode, warnings may cause build to fail + with pytest.raises((AssertionError, Exception)): + sphinx_build( + srcdir="with_issues", + confoverrides=confoverrides + ).build(raise_on_warning=True) + else: + # Without strict mode, warnings are logged but build succeeds + result = build_and_capture_stdout(sphinx_build, capsys, + srcdir="with_issues", + confoverrides=confoverrides + ) + assert result.pdf_exists() diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..910fe82 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,14 @@ +from pdfminer.high_level import extract_text + +def extract_pdf_text(pdf_path): + """Extrahiere gesamten Text aus PDF.""" + return extract_text(str(pdf_path)) + + + +def build_and_capture_stdout(sphinx_build, capsys, srcdir, **kwargs): + """Baue das PDF und liefere das captured stdout zurück.""" + result = sphinx_build(srcdir=srcdir, **kwargs).build() + captured = capsys.readouterr() + result.warnings = captured.out.splitlines() + return result From df627c64814bc65902e288f7af704d88eb044125 Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Tue, 25 Nov 2025 14:46:39 +0100 Subject: [PATCH 02/26] adding tests for html processings --- tests/conftest.py | 62 ++++++++++++++++++++++++++--------- tests/test_html_processing.py | 11 ++++--- tests/utils.py | 18 ++++++++-- 3 files changed, 67 insertions(+), 24 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 49d5b1b..174ed82 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,10 +2,14 @@ from pathlib import Path from typing import Any import shutil +import io +import re import pytest from sphinx.application import Sphinx from sphinx.testing.util import SphinxTestApp +from sphinx.util import logging as sphinx_logging +import logging pytest_plugins = ("sphinx.testing.fixtures",) @@ -53,14 +57,17 @@ def test_example(app): class SphinxBuild: """Helper class to build Sphinx documentation and access results.""" - def __init__(self, app: Sphinx, src: Path): + def __init__(self, app: Sphinx, src: Path, status, warning): self.app = app self.src = src self.outdir = None self.warnings = [] self.errors = [] + self.status_stream = status + self.warning_stream = warning + self.debug_output = [] - def build(self, force_all: bool = True, raise_on_warning: bool = False): + def build(self, force_all: bool = True, raise_on_warning: bool = False, debug: bool = False): """ Build the documentation. @@ -68,17 +75,31 @@ def build(self, force_all: bool = True, raise_on_warning: bool = False): force_all: Rebuild all files raise_on_warning: Raise exception on warnings """ + + if debug: + self.app.verbosity = 2 + + debug_buffer = io.StringIO() if debug else None + + # Eigenen Handler OHNE Sphinx-Filter für Debug + debug_handler = logging.StreamHandler(debug_buffer) + debug_handler.setLevel(logging.DEBUG) + debug_handler.setFormatter(logging.Formatter('%(levelname)s: %(message)s')) + + # An Extension-Logger hängen (umgeht Sphinx-Filter!) + ext_logger = sphinx_logging.getLogger("sphinx_simplepdf") + ext_logger.logger.addHandler(debug_handler) + ext_logger.logger.setLevel(logging.DEBUG) + ext_logger.logger.propagate = False # Verhindere Doppel-Logging + self.app.build(force_all=force_all) self.outdir = Path(self.app.builder.outdir) - # Collect warnings and errors from status/warning streams - warning_content = self.app._warning.getvalue() - if warning_content: - self.warnings = [ - line.strip() - for line in warning_content.split("\n") - if line.strip() - ] + self.status_output = self.status_stream.getvalue() + self.warnings = self.warning_stream.getvalue().splitlines() + + if debug: + self.debug_output = debug_buffer.getvalue().splitlines() if raise_on_warning and self.warnings: raise AssertionError( @@ -114,14 +135,20 @@ def processed_html(self) -> str: """ # Look for debug HTML output in warnings # (SimplePDF should log processed HTML with specific marker) - for i, line in enumerate(self.warnings): + + def strip_log_prefix(text): + """Entferne Log-Level-Prefixe wie 'DEBUG: ', 'INFO: ' etc.""" + return re.sub(r'^(DEBUG|INFO|WARNING|ERROR|CRITICAL):\s*', '', text, flags=re.MULTILINE) + + # print(f"-- debug output--\n{self.debug_output}\n-----") + for i, line in enumerate(self.debug_output): if "DEBUG HTML START" in line: # Find end marker html_lines = [] - for next_line in self.warnings[i+1:]: + for next_line in self.debug_output[i+1:]: if "DEBUG HTML END" in next_line: break - html_lines.append(next_line) + html_lines.append(strip_log_prefix(next_line)) return "\n".join(html_lines) raise ValueError("No processed HTML found in debug output") @@ -228,18 +255,21 @@ def _make_build( if not src_path.exists(): raise ValueError(f"Test document directory not found: {src_path}") - print(f"using src_dir {src_dir}") + status = io.StringIO() + warning = io.StringIO() # Create app with Path object (Sphinx 8.2 compatible) app = make_app( buildername=buildername, # srcdir=src_path, srcdir=src_dir, + status=status, + warning=warning, confoverrides=confoverrides or {}, + freshenv = True, **kwargs ) - - return SphinxBuild(app, src_path) + return SphinxBuild(app, src_path, status, warning) return _make_build diff --git a/tests/test_html_processing.py b/tests/test_html_processing.py index bb78004..02b05f0 100644 --- a/tests/test_html_processing.py +++ b/tests/test_html_processing.py @@ -1,11 +1,11 @@ """Tests for HTML processing in SimplePDF builder.""" import pytest import re +from .utils import build_and_capture_stdout, prettify_html -def test_html_is_processed(sphinx_build): - """Test that HTML is processed by SimplePDF builder.""" - result = sphinx_build(srcdir="basic_doc").build() +def test_html_is_processed(sphinx_build, capsys): + result = build_and_capture_stdout(sphinx_build, capsys, srcdir="basic_doc", build_kwargs={"debug":True}) # Original HTML should exist original_html = result.html_content("index") @@ -15,9 +15,10 @@ def test_html_is_processed(sphinx_build): # (requires SimplePDF to log processed HTML) try: processed_html = result.processed_html() + pretty_original = prettify_html(original_html) assert processed_html - # Processed HTML should differ from original - assert processed_html != original_html + # no toctree, no fix! + assert processed_html == pretty_original except ValueError: pytest.skip("SimplePDF debug output not available") diff --git a/tests/utils.py b/tests/utils.py index 910fe82..492d46f 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,14 +1,26 @@ +import re from pdfminer.high_level import extract_text +from bs4 import BeautifulSoup + +# ANSI_ESCAPE_RE = re.compile(r'\x1b\\[[;?0-9]*[A-Za-z]?') +# +# def strip_ansi(text): +# return ANSI_ESCAPE_RE.sub('', text) def extract_pdf_text(pdf_path): """Extrahiere gesamten Text aus PDF.""" return extract_text(str(pdf_path)) +def prettify_html(html): + soup = BeautifulSoup(html, "html.parser") + return str(soup.prettify(formatter="html")) + -def build_and_capture_stdout(sphinx_build, capsys, srcdir, **kwargs): +def build_and_capture_stdout(sphinx_build, capsys, srcdir, build_kwargs=None, **sphinx_kwargs): """Baue das PDF und liefere das captured stdout zurück.""" - result = sphinx_build(srcdir=srcdir, **kwargs).build() + build_kwargs = build_kwargs or {} + result = sphinx_build(srcdir=srcdir, **sphinx_kwargs).build(**build_kwargs) captured = capsys.readouterr() - result.warnings = captured.out.splitlines() + result.warnings += captured.out.splitlines() return result From 1bc75dbdf2ac29d3bdeb3ebf79f3affabedd2198 Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Tue, 25 Nov 2025 15:01:31 +0100 Subject: [PATCH 03/26] use specifix sphinx versions --- pyproject.toml | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 398da0c..ec3432d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ dev = [ "tox>=4.0", "tox-uv>=1.0", "ruff>=0.4.0", - "pdfminer.six" + "pdfminer.six>=20220319" ] docs = [ "sphinx>=4.0", @@ -111,11 +111,30 @@ requires = deps = sphinx>=4.0 pytest - pdfminer.six + pdfminer.six>=20220319 sphinx-simplepdf @ . commands = pytest tests/ +[testenv:py{310,311,312,313}-sphinx74] +deps = + sphinx>=7.4,<7.5 + pytest + pdfminer.six>=20220319 + sphinx-simplepdf @ . +commands = + pytest tests/ + +[testenv:py{310,311,312,313}-sphinx82] +deps = + sphinx>=8.2,<8.3 + pytest + pdfminer.six>=20220319 + sphinx-simplepdf @ . +commands = + pytest tests/ + + [testenv:lint] description = Run code quality checks commands = From f9cdec241876409192ecb8d11bdb8fe1304558de Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Tue, 25 Nov 2025 19:51:49 +0100 Subject: [PATCH 04/26] [#120] fix page break handling for toc entries (break on span, not hx) --- sphinx_simplepdf/builders/simplepdf.py | 29 +++++++++++++--------- tests/doc_test/with_toc/_static/custom.css | 20 +++++++++++++++ tests/doc_test/with_toc/conf.py | 2 ++ tests/test_pdf_generation.py | 18 ++++++++------ tests/utils.py | 13 +++++----- 5 files changed, 56 insertions(+), 26 deletions(-) create mode 100644 tests/doc_test/with_toc/_static/custom.css diff --git a/sphinx_simplepdf/builders/simplepdf.py b/sphinx_simplepdf/builders/simplepdf.py index a40478b..d8ad02c 100644 --- a/sphinx_simplepdf/builders/simplepdf.py +++ b/sphinx_simplepdf/builders/simplepdf.py @@ -340,20 +340,25 @@ def _toctree_fix(self, html): continue for heading_tag in ["h1", "h2"]: - headings = soup.find_all(heading_tag, class_="") + headings = soup.find_all(heading_tag) for number, heading in enumerate(headings): class_attr = heading.attrs["class"] if heading.has_attr("class") else [] - logger.debug(f"found heading {heading}") - if 0 == number: - class_attr.append("first") - if 0 == number % 2: - class_attr.append("even") - else: - class_attr.append("odd") - if len(headings) - 1 == number: - class_attr.append("last") - - heading.attrs["class"] = class_attr + parent = heading.find_parent("section") + # is the parent a section + if parent and parent.name == 'section': + prev_span = parent.find_previous_sibling('span', id=True) + # check if previous sibling is span with an id + if prev_span: + # add classes for css handling + prev_span['class'] = prev_span.get('class', []) + ['anchor-before-heading'] + if 0 == number: + prev_span['class'] = prev_span.get('class', []) + ["first"] + if 0 == number % 2: + prev_span['class'] = prev_span.get('class', []) + ["even"] + else: + prev_span['class'] = prev_span.get('class', []) + ["odd"] + if len(headings) - 1 == number: + prev_span['class'] = prev_span.get('class', []) + ["last"] logger.debug("DEBUG HTML START") logger.debug(soup.prettify(formatter="html")) diff --git a/tests/doc_test/with_toc/_static/custom.css b/tests/doc_test/with_toc/_static/custom.css new file mode 100644 index 0000000..867208c --- /dev/null +++ b/tests/doc_test/with_toc/_static/custom.css @@ -0,0 +1,20 @@ +div.body h1{ + display: none; +} + +h2 { + page-break-before: avoid !important; + break-before:avoid !important; +} + +span.anchor-before-heading { + display: block; + page-break-before: always; + height: 0; + margin: 0; + padding: 0; +} + +span.anchor-before-heading.first { + page-break-before: avoid !important; +} diff --git a/tests/doc_test/with_toc/conf.py b/tests/doc_test/with_toc/conf.py index 29bfc77..92a8a38 100644 --- a/tests/doc_test/with_toc/conf.py +++ b/tests/doc_test/with_toc/conf.py @@ -6,3 +6,5 @@ simplepdf_theme = "simplepdf_theme" simplepdf_toc = True +html_css_files = ["custom.css" ] +html_static_path = ['_static'] diff --git a/tests/test_pdf_generation.py b/tests/test_pdf_generation.py index 7522428..3505642 100644 --- a/tests/test_pdf_generation.py +++ b/tests/test_pdf_generation.py @@ -1,7 +1,7 @@ """Tests for PDF generation with WeasyPrint.""" import pytest from pathlib import Path -from .utils import build_and_capture_stdout, extract_pdf_text +from .utils import build_and_capture_stdout, extract_pdf_text, page_count def test_pdf_is_created(sphinx_build, capsys): @@ -36,7 +36,7 @@ def test_pdf_with_images(sphinx_build, capsys): def test_pdf_with_toc(sphinx_build, capsys): """Test that PDF with table of contents is generated.""" # result = sphinx_build(srcdir="with_toc").build() - result = build_and_capture_stdout(sphinx_build, capsys, srcdir="with_toc") + result = build_and_capture_stdout(sphinx_build, capsys, srcdir="with_toc", build_kwargs={"debug": True}) assert result.pdf_exists() # TOC usually makes PDF larger @@ -47,6 +47,8 @@ def test_pdf_with_toc(sphinx_build, capsys): anchor_warnings = result.get_warnings_matching(r"(anchor|link|reference)") assert len(anchor_warnings) == 0 + assert 5 == page_count(pdf_path) + text = extract_pdf_text(pdf_path) assert """ @@ -64,17 +66,17 @@ def test_pdf_with_toc(sphinx_build, capsys): • Section 2.1 • Section 2.2 -4 +3 -4 +3 -4 +3 -5 +4 -5 +4 -5 +4 """ in text diff --git a/tests/utils.py b/tests/utils.py index 492d46f..aa6991a 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,16 +1,17 @@ -import re -from pdfminer.high_level import extract_text +from pdfminer.high_level import extract_text, extract_pages from bs4 import BeautifulSoup -# ANSI_ESCAPE_RE = re.compile(r'\x1b\\[[;?0-9]*[A-Za-z]?') -# -# def strip_ansi(text): -# return ANSI_ESCAPE_RE.sub('', text) def extract_pdf_text(pdf_path): """Extrahiere gesamten Text aus PDF.""" return extract_text(str(pdf_path)) +def page_count(pdf_path): + """get page count of pdf""" + page_count = 0 + for page_layout in extract_pages(pdf_path): + page_count += 1 + return page_count def prettify_html(html): soup = BeautifulSoup(html, "html.parser") From c95512961064d935ccaf3f3629d80859051e0570 Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Wed, 26 Nov 2025 10:22:21 +0100 Subject: [PATCH 05/26] add support for tox: demo, doc, lint --- demo/doc-requirements.txt | 3 -- docs/Makefile | 20 ---------- docs/conf.py | 4 +- docs/directives.rst | 12 +++--- docs/doc-requirements.txt | 4 -- docs/index.rst | 4 +- docs/make.bat | 35 ------------------ pyproject.toml | 51 +++++++++++++++++++++----- sphinx_simplepdf/builders/simplepdf.py | 7 +++- tests/test_pdf_generation.py | 2 - 10 files changed, 57 insertions(+), 85 deletions(-) delete mode 100644 demo/doc-requirements.txt delete mode 100644 docs/Makefile delete mode 100644 docs/doc-requirements.txt delete mode 100644 docs/make.bat diff --git a/demo/doc-requirements.txt b/demo/doc-requirements.txt deleted file mode 100644 index df198c4..0000000 --- a/demo/doc-requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -sphinx==5.1.1 -sphinxcontrib-plantuml==0.24 -sphinx-needs==1.0.2 diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index d4bb2cb..0000000 --- a/docs/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = . -BUILDDIR = _build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/conf.py b/docs/conf.py index cfd2f6a..fc5c881 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -20,14 +20,14 @@ extensions = [ 'sphinx_simplepdf', 'sphinxcontrib.plantuml', - 'sphinxcontrib.needs', + 'sphinx_needs', 'sphinx_copybutton', ] version = "1.6.0" templates_path = ['_templates'] -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', 'if_pdf_include.rst'] plantuml_output_format = "svg_img" local_plantuml_path = os.path.join(os.path.dirname(__file__), "utils", "plantuml.jar") diff --git a/docs/directives.rst b/docs/directives.rst index 99a5d95..fc84ac5 100644 --- a/docs/directives.rst +++ b/docs/directives.rst @@ -68,7 +68,7 @@ if-include include nested in a if-builder statement. You can list multiple files and use different builders. .. code-block:: rst - + .. if-builder:: simplepdf .. include:: ./path/to/my/file.xy @@ -79,9 +79,9 @@ include nested in a if-builder statement. You can list multiple files and use di is the same as .. code-block:: rst - - .. if-include:: simplepdf - + + .. if-include:: simplepdf + ./path/to/my/file.xy ./path/to/my/other/file.xy @@ -94,9 +94,9 @@ is the same as The following chapter should only be visible in the PDF version of this documentation -.. if-include:: simplepdf +.. if-include:: simplepdf - ./if_pdf_include.rst + .. include:: if_pdf_include.rst .. _pdf-include: diff --git a/docs/doc-requirements.txt b/docs/doc-requirements.txt deleted file mode 100644 index 067b274..0000000 --- a/docs/doc-requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -sphinx==5.1.1 -sphinxcontrib-plantuml==0.24 -sphinxcontrib-needs==0.7.8 -sphinx-copybutton \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index a1ce0b6..989133b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -99,6 +99,6 @@ Showcase directives css tech_details - examples/index + examples/sphinx_needs changelog - license \ No newline at end of file + license diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 954237b..0000000 --- a/docs/make.bat +++ /dev/null @@ -1,35 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=. -set BUILDDIR=_build - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.https://www.sphinx-doc.org/ - exit /b 1 -) - -if "%1" == "" goto help - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd diff --git a/pyproject.toml b/pyproject.toml index ec3432d..860fd5f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ classifiers = [ "Framework :: Sphinx :: Extension", ] dependencies = [ - "sphinx>=4.0", + "sphinx>=7.0", "weasyprint>=66.0", "libsass", "beautifulsoup4", @@ -35,7 +35,6 @@ dependencies = [ "pillow>=9.1.0", "pydyf>=0.10.0", "cffi>=0.6", - "pycairo", ] [project.entry-points."sphinx.html_themes"] @@ -56,12 +55,20 @@ dev = [ "pdfminer.six>=20220319" ] docs = [ - "sphinx>=4.0", + "sphinx>=7.0", "sphinxcontrib-plantuml", "sphinx-needs", "sphinx-copybutton", ] +demo = [ + "sphinx>=7.0", + "sphinxcontrib-plantuml", + "sphinx-needs", + "sphinx-copybutton", +] + + [project.urls] Homepage = "https://github.com/useblocks/sphinx-simplepdf" Documentation = "https://sphinx-simplepdf.readthedocs.io/en/latest/" @@ -100,7 +107,7 @@ force-sort-within-sections = true legacy_tox_ini = """ [tox] env_list = - py{310,311,312,313}-sphinx{74,82} + py{311,312,313}-sphinx{74,81,82} docs lint requires = @@ -109,12 +116,12 @@ requires = [testenv] deps = - sphinx>=4.0 + sphinx>=7.0 pytest pdfminer.six>=20220319 sphinx-simplepdf @ . commands = - pytest tests/ + pytest {posargs:tests/} [testenv:py{310,311,312,313}-sphinx74] deps = @@ -123,20 +130,32 @@ deps = pdfminer.six>=20220319 sphinx-simplepdf @ . commands = - pytest tests/ + pytest {posargs:tests/} -[testenv:py{310,311,312,313}-sphinx82] +[testenv:py{311,312,313}-sphinx81] deps = - sphinx>=8.2,<8.3 + sphinx>=8.1,<8.2 pytest pdfminer.six>=20220319 sphinx-simplepdf @ . commands = - pytest tests/ + pytest {posargs:tests/} +[testenv:py{311,312,313}-sphinx82] +deps = + sphinx>=8.2,<8.3 + pytest + pdfminer.six>=20220319 + sphinx-simplepdf @ . +commands = + pytest {posargs:tests/} [testenv:lint] description = Run code quality checks +allowlist_externals = + ruff +deps = + ruff>=0.4.0 commands = ruff check sphinx_simplepdf/ ruff format --check sphinx_simplepdf/ @@ -149,8 +168,20 @@ deps = extras = docs commands = sphinx-build -a -E -j auto -b html docs/ docs/_build + +[testenv:demo] +description = Build demo with Sphinx +basepython = python3.12 +deps = + sphinx>=7.0,<8.0 +extras = demo +commands = + sphinx-build -a -E -j auto -b simplepdf demo/ demo/_build + """ + + # [tool.setuptools.packages.find] # where = ["sphinx_simplepdf/themes/simplepdf_theme"] diff --git a/sphinx_simplepdf/builders/simplepdf.py b/sphinx_simplepdf/builders/simplepdf.py index d8ad02c..58fc39f 100644 --- a/sphinx_simplepdf/builders/simplepdf.py +++ b/sphinx_simplepdf/builders/simplepdf.py @@ -342,7 +342,12 @@ def _toctree_fix(self, html): for heading_tag in ["h1", "h2"]: headings = soup.find_all(heading_tag) for number, heading in enumerate(headings): - class_attr = heading.attrs["class"] if heading.has_attr("class") else [] + class_attr = heading.get('class', []) + if 0 == number: + class_attr +=["first"] + class_attr += [f"heading-{number}"] + heading['class'] = class_attr + parent = heading.find_parent("section") # is the parent a section if parent and parent.name == 'section': diff --git a/tests/test_pdf_generation.py b/tests/test_pdf_generation.py index 3505642..2e2964e 100644 --- a/tests/test_pdf_generation.py +++ b/tests/test_pdf_generation.py @@ -39,9 +39,7 @@ def test_pdf_with_toc(sphinx_build, capsys): result = build_and_capture_stdout(sphinx_build, capsys, srcdir="with_toc", build_kwargs={"debug": True}) assert result.pdf_exists() - # TOC usually makes PDF larger pdf_path = result.pdf_path() - assert pdf_path.stat().st_size > 2000 # Check for specific WeasyPrint anchor warnings anchor_warnings = result.get_warnings_matching(r"(anchor|link|reference)") From 5ba3ee129302b1037c2486990d44f729c5e412cb Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Mon, 26 Jan 2026 19:00:21 +0100 Subject: [PATCH 06/26] fix typo in uv sync --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 873da9c..c7301a0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -49,7 +49,7 @@ jobs: - name: Set up Python run: uv python install ${{ matrix.python }} - name: Install docs dependencies - run: uv sync --groups docs + run: uv sync --group docs - name: Build docs (${{ matrix.target }}) run: uv run sphinx-build -M ${{ matrix.builder }} docs docs/_build shell: bash From 7859f661581a86a801eea842aef64cf9c79936c5 Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Mon, 26 Jan 2026 19:10:30 +0100 Subject: [PATCH 07/26] Adding tests to ci execution --- .github/workflows/ci.yaml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c7301a0..4ec4e76 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -55,11 +55,40 @@ jobs: shell: bash env: PATH: ${{ runner.os == 'Windows' && 'C:\msys64\mingw64\bin;' || '' }}${{ env.PATH }} + tests: + name: tests-${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + steps: + - uses: actions/checkout@v4 + - name: Install WeasyPrint dependencies (Ubuntu) + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y libpango-1.0-0 libpangocairo-1.0-0 libgdk-pixbuf2.0-0 libffi-dev libcairo2 + - name: Install WeasyPrint dependencies (macOS) + if: runner.os == 'macOS' + run: brew install pango libffi + - name: Install WeasyPrint dependencies (Windows) + if: runner.os == 'Windows' + run: | + $env:PATH = "C:\msys64\mingw64\bin;$env:PATH" + C:\msys64\usr\bin\pacman -S --noconfirm mingw-w64-x86_64-pango mingw-w64-x86_64-gdk-pixbuf2 + shell: pwsh + - name: Install uv + uses: astral-sh/setup-uv@v5 + - name: Testing (${{ matrix.os }}) + run: uv sync --group dev && uv run tox + shell: bash + env: + PATH: ${{ runner.os == 'Windows' && 'C:\msys64\mingw64\bin;' || '' }}${{ env.PATH }} all_good: if: ${{ !cancelled() }} needs: - pre-commit - docs + - tests runs-on: ubuntu-latest steps: - name: Decide whether the needed jobs succeeded or failed From d414006d520192c56199f29bbac6f96b6982f43f Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Mon, 26 Jan 2026 19:16:59 +0100 Subject: [PATCH 08/26] remove demo from tests, it is a requirement for the docs build --- pyproject.toml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ec691ef..67b3c01 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -120,7 +120,6 @@ legacy_tox_ini = """ env_list = lint py{311,312,313}-sphinx{74,81} #82 not working, see https://github.com/sphinx-doc/sphinx/pull/13739 - demo requires = tox>=4 tox-uv>=1.0 @@ -171,15 +170,6 @@ commands = ruff check sphinx_simplepdf/ ruff format --check sphinx_simplepdf/ -[testenv:demo] -description = Build demo with Sphinx -basepython = python3.12 -deps = - sphinx>=7.0,<8.2 -extras = demo -commands = - sphinx-build -a -E -j auto -b simplepdf demo/ demo/_build - """ # [tool.setuptools.packages.find] From 40bb9b4db93b122930745471b074c00ae8b1a4f0 Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Mon, 26 Jan 2026 19:18:23 +0100 Subject: [PATCH 09/26] add demo build as requirement for docs build --- .github/workflows/ci.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4ec4e76..ec51830 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -51,7 +51,9 @@ jobs: - name: Install docs dependencies run: uv sync --group docs - name: Build docs (${{ matrix.target }}) - run: uv run sphinx-build -M ${{ matrix.builder }} docs docs/_build + run: | + uv run sphinx-build -M simplepdf demo demo/_build + uv run sphinx-build -M ${{ matrix.builder }} docs docs/_build shell: bash env: PATH: ${{ runner.os == 'Windows' && 'C:\msys64\mingw64\bin;' || '' }}${{ env.PATH }} From a1b9791be7ccd1b4e0e131d6d0c8154faffd6f58 Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Mon, 26 Jan 2026 19:35:29 +0100 Subject: [PATCH 10/26] run tests only on ubuntu to get a successfull result --- .github/workflows/ci.yaml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ec51830..972ce0a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -52,7 +52,7 @@ jobs: run: uv sync --group docs - name: Build docs (${{ matrix.target }}) run: | - uv run sphinx-build -M simplepdf demo demo/_build + uv run sphinx-build -M ${{ matrix.builder }} demo demo/_build uv run sphinx-build -M ${{ matrix.builder }} docs docs/_build shell: bash env: @@ -63,7 +63,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: [ubuntu-latest] steps: - uses: actions/checkout@v4 - name: Install WeasyPrint dependencies (Ubuntu) @@ -80,8 +80,10 @@ jobs: shell: pwsh - name: Install uv uses: astral-sh/setup-uv@v5 + - name: Install tests dependencies + run: uv sync --group dev - name: Testing (${{ matrix.os }}) - run: uv sync --group dev && uv run tox + run: uv run tox shell: bash env: PATH: ${{ runner.os == 'Windows' && 'C:\msys64\mingw64\bin;' || '' }}${{ env.PATH }} From c5f79367cb4009d2cee2fd684b1722e28fef67b9 Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Mon, 26 Jan 2026 19:48:17 +0100 Subject: [PATCH 11/26] remove demo project build dependency --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 972ce0a..421562f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -50,9 +50,9 @@ jobs: run: uv python install ${{ matrix.python }} - name: Install docs dependencies run: uv sync --group docs + working-directory: . - name: Build docs (${{ matrix.target }}) run: | - uv run sphinx-build -M ${{ matrix.builder }} demo demo/_build uv run sphinx-build -M ${{ matrix.builder }} docs docs/_build shell: bash env: From 3331950fa20ce28d587859c94964ba347d13c972 Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Tue, 27 Jan 2026 14:12:43 +0100 Subject: [PATCH 12/26] try to fix weasyprint doc build with updated environment --- .github/workflows/ci.yaml | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 421562f..3ef2561 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -40,10 +40,22 @@ jobs: run: brew install pango libffi - name: Install WeasyPrint dependencies (Windows) if: runner.os == 'Windows' - run: | - $env:PATH = "C:\msys64\mingw64\bin;$env:PATH" - C:\msys64\usr\bin\pacman -S --noconfirm mingw-w64-x86_64-pango mingw-w64-x86_64-gdk-pixbuf2 - shell: pwsh + uses: msys2/setup-msys2@v2 + with: + update: true + install: > + mingw-w64-x86_64-pango + mingw-w64-x86_64-gdk-pixbuf2 + mingw-w64-x86_64-cairo + mingw-w64-x86_64-fontconfig + mingw-w64-x86_64-harfbuzz + mingw-w64-x86_64-freetype + mingw-w64-x86_64-libpng + mingw-w64-x86_64-libjpeg-turbo + mingw-w64-x86_64-libffi + mingw-w64-x86_64-glib2 + env: + PATH: C:\msys64\mingw64\bin;${{ env.PATH }} - name: Install uv uses: astral-sh/setup-uv@v5 - name: Set up Python From 4fe3a186256339399252cd673454b1aad80603fc Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Tue, 27 Jan 2026 14:29:09 +0100 Subject: [PATCH 13/26] fix cmd error --- .github/workflows/ci.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3ef2561..8f5aaa4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -41,6 +41,7 @@ jobs: - name: Install WeasyPrint dependencies (Windows) if: runner.os == 'Windows' uses: msys2/setup-msys2@v2 + shell: msys2 {0} with: update: true install: > From 7c309f5e087ad2a449e34dbf1d9eb91745f98183 Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Tue, 27 Jan 2026 16:46:08 +0100 Subject: [PATCH 14/26] use mingw install step from WeasyPrint CI yaml --- .github/workflows/ci.yaml | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8f5aaa4..7249636 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -40,21 +40,9 @@ jobs: run: brew install pango libffi - name: Install WeasyPrint dependencies (Windows) if: runner.os == 'Windows' - uses: msys2/setup-msys2@v2 - shell: msys2 {0} - with: - update: true - install: > - mingw-w64-x86_64-pango - mingw-w64-x86_64-gdk-pixbuf2 - mingw-w64-x86_64-cairo - mingw-w64-x86_64-fontconfig - mingw-w64-x86_64-harfbuzz - mingw-w64-x86_64-freetype - mingw-w64-x86_64-libpng - mingw-w64-x86_64-libjpeg-turbo - mingw-w64-x86_64-libffi - mingw-w64-x86_64-glib2 + run: | + C:\msys64\usr\bin\bash -lc 'pacman -S mingw-w64-x86_64-ttf-dejavu mingw-w64-x86_64-pango mingw-w64-x86_64-ghostscript --noconfirm' + xcopy "C:\msys64\mingw64\share\fonts\TTF" "C:\Users\runneradmin\.fonts" /e /i env: PATH: C:\msys64\mingw64\bin;${{ env.PATH }} - name: Install uv From 4e6a1048c847c905146b9121b85c19501007fa61 Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Tue, 27 Jan 2026 16:49:13 +0100 Subject: [PATCH 15/26] avoid the xcopy step --- .github/workflows/ci.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7249636..1f5eda9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -42,7 +42,6 @@ jobs: if: runner.os == 'Windows' run: | C:\msys64\usr\bin\bash -lc 'pacman -S mingw-w64-x86_64-ttf-dejavu mingw-w64-x86_64-pango mingw-w64-x86_64-ghostscript --noconfirm' - xcopy "C:\msys64\mingw64\share\fonts\TTF" "C:\Users\runneradmin\.fonts" /e /i env: PATH: C:\msys64\mingw64\bin;${{ env.PATH }} - name: Install uv From 3974ccc448d2b4e767becd38bf92b042bd8a8f83 Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Tue, 27 Jan 2026 17:03:41 +0100 Subject: [PATCH 16/26] set explizit dll directory --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1f5eda9..a6e999f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -42,6 +42,7 @@ jobs: if: runner.os == 'Windows' run: | C:\msys64\usr\bin\bash -lc 'pacman -S mingw-w64-x86_64-ttf-dejavu mingw-w64-x86_64-pango mingw-w64-x86_64-ghostscript --noconfirm' + echo "WEASYPRINT_DLL_DIRECTORIES=C:\msys64\mingw64\bin" >> $GITHUB_ENV env: PATH: C:\msys64\mingw64\bin;${{ env.PATH }} - name: Install uv @@ -54,7 +55,6 @@ jobs: - name: Build docs (${{ matrix.target }}) run: | uv run sphinx-build -M ${{ matrix.builder }} docs docs/_build - shell: bash env: PATH: ${{ runner.os == 'Windows' && 'C:\msys64\mingw64\bin;' || '' }}${{ env.PATH }} tests: From 77b30f2d2558dd534050ed76df615fa7139c4346 Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Wed, 28 Jan 2026 12:18:02 +0100 Subject: [PATCH 17/26] use msys compatible path for WEASYPRINT_DLL_DIRECTORIES --- .github/workflows/ci.yaml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a6e999f..64c1b70 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -42,7 +42,7 @@ jobs: if: runner.os == 'Windows' run: | C:\msys64\usr\bin\bash -lc 'pacman -S mingw-w64-x86_64-ttf-dejavu mingw-w64-x86_64-pango mingw-w64-x86_64-ghostscript --noconfirm' - echo "WEASYPRINT_DLL_DIRECTORIES=C:\msys64\mingw64\bin" >> $GITHUB_ENV + echo "WEASYPRINT_DLL_DIRECTORIES=/c/msys64/mingw64/bin" >> $GITHUB_ENV env: PATH: C:\msys64\mingw64\bin;${{ env.PATH }} - name: Install uv diff --git a/pyproject.toml b/pyproject.toml index 67b3c01..4d8dfa7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ classifiers = [ dependencies = [ "pre-commit>=3.5", "sphinx>=7.0", - "weasyprint>=67.0", + "weasyprint==67.0", "libsass", "beautifulsoup4", "packaging>=20.0", # Modern replacement for pkg_resources.parse_version From 481bd95e9c912a62c1c770035e0f9cdc4a716ecd Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Wed, 28 Jan 2026 17:14:47 +0100 Subject: [PATCH 18/26] set WEASYPRINT_DLL_DIRECTORIES explicitly --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 64c1b70..7603e57 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -42,7 +42,6 @@ jobs: if: runner.os == 'Windows' run: | C:\msys64\usr\bin\bash -lc 'pacman -S mingw-w64-x86_64-ttf-dejavu mingw-w64-x86_64-pango mingw-w64-x86_64-ghostscript --noconfirm' - echo "WEASYPRINT_DLL_DIRECTORIES=/c/msys64/mingw64/bin" >> $GITHUB_ENV env: PATH: C:\msys64\mingw64\bin;${{ env.PATH }} - name: Install uv @@ -57,6 +56,7 @@ jobs: uv run sphinx-build -M ${{ matrix.builder }} docs docs/_build env: PATH: ${{ runner.os == 'Windows' && 'C:\msys64\mingw64\bin;' || '' }}${{ env.PATH }} + WEASYPRINT_DLL_DIRECTORIES: ${{ runner.os == 'Windows' && 'C:\msys64\mingw64\bin;' || '' }} tests: name: tests-${{ matrix.os }} runs-on: ${{ matrix.os }} From 38bc90f4db56d43fc1b60daf479749445661c4c4 Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Thu, 29 Jan 2026 12:23:16 +0100 Subject: [PATCH 19/26] [] giving up to fix windows doc builds --- .github/workflows/ci.yaml | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7603e57..239856f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -41,23 +41,20 @@ jobs: - name: Install WeasyPrint dependencies (Windows) if: runner.os == 'Windows' run: | - C:\msys64\usr\bin\bash -lc 'pacman -S mingw-w64-x86_64-ttf-dejavu mingw-w64-x86_64-pango mingw-w64-x86_64-ghostscript --noconfirm' - env: - PATH: C:\msys64\mingw64\bin;${{ env.PATH }} + $env:PATH = "C:\msys64\mingw64\bin;$env:PATH" + C:\msys64\usr\bin\pacman -S --noconfirm mingw-w64-x86_64-pango mingw-w64-x86_64-gdk-pixbuf2 + shell: pwsh - name: Install uv uses: astral-sh/setup-uv@v5 - name: Set up Python run: uv python install ${{ matrix.python }} - name: Install docs dependencies run: uv sync --group docs - working-directory: . - name: Build docs (${{ matrix.target }}) - run: | - uv run sphinx-build -M ${{ matrix.builder }} docs docs/_build + run: uv run sphinx-build -M ${{ matrix.builder }} docs docs/_build + shell: bash env: - PATH: ${{ runner.os == 'Windows' && 'C:\msys64\mingw64\bin;' || '' }}${{ env.PATH }} - WEASYPRINT_DLL_DIRECTORIES: ${{ runner.os == 'Windows' && 'C:\msys64\mingw64\bin;' || '' }} - tests: + PATH: ${{ runner.os == 'Windows' && 'C:\msys64\mingw64\bin;' || '' }}${{ env.PATH }} tests: name: tests-${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: From 6611ddd3309702f0ff298cd4ecf2e47ec2cd1149 Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Thu, 29 Jan 2026 12:36:18 +0100 Subject: [PATCH 20/26] fix eol error in yaml --- .github/workflows/ci.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 239856f..445ddbb 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -54,7 +54,8 @@ jobs: run: uv run sphinx-build -M ${{ matrix.builder }} docs docs/_build shell: bash env: - PATH: ${{ runner.os == 'Windows' && 'C:\msys64\mingw64\bin;' || '' }}${{ env.PATH }} tests: + PATH: ${{ runner.os == 'Windows' && 'C:\msys64\mingw64\bin;' || '' }}${{ env.PATH }} + tests: name: tests-${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: From 24fe9cb84cbcbd0cc7dac8ac443bf804bfd5430a Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Thu, 29 Jan 2026 19:43:20 +0100 Subject: [PATCH 21/26] [#130] use different font-family --- .../simplepdf_theme/static/styles/sources/_fonts.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sphinx_simplepdf/themes/simplepdf_theme/static/styles/sources/_fonts.scss b/sphinx_simplepdf/themes/simplepdf_theme/static/styles/sources/_fonts.scss index 9d0e0a4..623b385 100644 --- a/sphinx_simplepdf/themes/simplepdf_theme/static/styles/sources/_fonts.scss +++ b/sphinx_simplepdf/themes/simplepdf_theme/static/styles/sources/_fonts.scss @@ -57,19 +57,19 @@ } @font-face { - font-family: monospace; + font-family: 'Fira Mono'; src: url(fonts/FiraMono-Regular.ttf); } @font-face { - font-family: monospace; + font-family: 'Fira Mono'; font-weight: 400; src: url(fonts/FiraMono-Medium.ttf); } @font-face { - font-family: monospace; + font-family: 'Fira Mono'; font-weight: bold; src: url(fonts/FiraMono-Bold.ttf); } From 85da97a13abb5cc218580b555c1fc92b79b99916 Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Thu, 29 Jan 2026 19:47:29 +0100 Subject: [PATCH 22/26] [#130] add test for proper pdf build on windows-latest --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 445ddbb..cee76b6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -61,7 +61,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest] + os: [ubuntu-latest, windows-latest] steps: - uses: actions/checkout@v4 - name: Install WeasyPrint dependencies (Ubuntu) From 40503341af572282126f344abd1c5c3e4fa18f99 Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Thu, 29 Jan 2026 19:23:34 +0000 Subject: [PATCH 23/26] fix font face definition according to css rules --- .../simplepdf_theme/static/styles/sources/_fonts.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sphinx_simplepdf/themes/simplepdf_theme/static/styles/sources/_fonts.scss b/sphinx_simplepdf/themes/simplepdf_theme/static/styles/sources/_fonts.scss index 623b385..8000d14 100644 --- a/sphinx_simplepdf/themes/simplepdf_theme/static/styles/sources/_fonts.scss +++ b/sphinx_simplepdf/themes/simplepdf_theme/static/styles/sources/_fonts.scss @@ -57,19 +57,19 @@ } @font-face { - font-family: 'Fira Mono'; + font-family: FiraMono, monospace; src: url(fonts/FiraMono-Regular.ttf); } @font-face { - font-family: 'Fira Mono'; + font-family: Fira Mono, monospace; font-weight: 400; src: url(fonts/FiraMono-Medium.ttf); } @font-face { - font-family: 'Fira Mono'; + font-family: Fira Mono, monospace; font-weight: bold; src: url(fonts/FiraMono-Bold.ttf); } From 646dc8e6aaa822fdc54941a12bc5f9c1c58f0041 Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Sat, 28 Feb 2026 18:47:12 +0100 Subject: [PATCH 24/26] [#83] add pdf comparison by image compare --- pyproject.toml | 12 + tests/conftest.py | 20 + tests/doc_test/with_toc/index.rst | 15 +- .../with_toc_and_typo/_static/custom.css | 20 + tests/doc_test/with_toc_and_typo/chapter1.rst | 17 + tests/doc_test/with_toc_and_typo/chapter2.rst | 17 + tests/doc_test/with_toc_and_typo/conf.py | 11 + tests/doc_test/with_toc_and_typo/index.rst | 20 + tests/imgcompare.py | 193 +++++++ tests/ref_pdfs/with_toc/TOCTest.pdf | Bin 0 -> 60355 bytes tests/test_pdf_generation.py | 56 +- uv.lock | 530 +++++++++++++++++- 12 files changed, 898 insertions(+), 13 deletions(-) create mode 100644 tests/doc_test/with_toc_and_typo/_static/custom.css create mode 100644 tests/doc_test/with_toc_and_typo/chapter1.rst create mode 100644 tests/doc_test/with_toc_and_typo/chapter2.rst create mode 100644 tests/doc_test/with_toc_and_typo/conf.py create mode 100644 tests/doc_test/with_toc_and_typo/index.rst create mode 100644 tests/imgcompare.py create mode 100644 tests/ref_pdfs/with_toc/TOCTest.pdf diff --git a/pyproject.toml b/pyproject.toml index 4d8dfa7..ec0d430 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,9 @@ dev = [ "tox-uv>=1.0", "ruff>=0.4.0", "pdfminer.six>=20220319", + "PyMuPDF>=1.24.0", # PDF -> Image for comparison + "opencv-python>=4.8.0", # cv2 for image processing + "scikit-image>=0.22.0", # ssim from skimage.metrics ] docs = [ "sphinx>=7.0,<8.2", @@ -138,6 +141,9 @@ deps = sphinx>=7.4,<7.5 pytest pdfminer.six>=20220319 + PyMuPDF>=1.24.0 + opencv-python>=4.8.0 + scikit-image>=0.22.0 sphinx-simplepdf @ . commands = pytest {posargs:tests/} @@ -147,6 +153,9 @@ deps = sphinx>=8.1,<8.2 pytest pdfminer.six>=20220319 + PyMuPDF>=1.24.0 + opencv-python>=4.8.0 + scikit-image>=0.22.0 sphinx-simplepdf @ . commands = pytest {posargs:tests/} @@ -156,6 +165,9 @@ deps = sphinx>=8.2,<8.3 pytest pdfminer.six>=20220319 + PyMuPDF>=1.24.0 + opencv-python>=4.8.0 + scikit-image>=0.22.0 sphinx-simplepdf @ . commands = pytest {posargs:tests/} diff --git a/tests/conftest.py b/tests/conftest.py index ad25a3e..e1b43e7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,6 +14,8 @@ from sphinx.testing.util import SphinxTestApp from sphinx.util import logging as sphinx_logging +from .imgcompare import compare_pdfs + pytest_plugins = ("sphinx.testing.fixtures",) @@ -42,6 +44,12 @@ def rootdir(): return Path(__file__).parent / "doc_test" +@pytest.fixture(scope="session") +def refdir(): + """Root directory for test documentation projects.""" + return Path(__file__).parent / "ref_pdfs" + + @pytest.fixture def content(request): """ @@ -69,6 +77,7 @@ def __init__(self, app: Sphinx, src: Path, status: io.StringIO, warning: io.Stri self.status_stream: io.StringIO = status self.warning_stream: io.StringIO = warning self.debug_output: list[str] = [] + self.pngs: list[Path] = [] def build(self, force_all: bool = True, raise_on_warning: bool = False, debug: bool = False): """ @@ -186,6 +195,17 @@ def pdf_exists(self, basename: str | None = None) -> bool: except FileNotFoundError: return False + def compare_pdf(self, ref_pdf: Path, changed_ratio_threshold: float = 0.001) -> bool: + if self.outdir is not None: + return compare_pdfs( + ref_pdf=ref_pdf, + test_pdf=self.pdf_path(), + work_dir=Path(self.outdir.parent / "png"), + changed_ratio_threshold=changed_ratio_threshold, + ) + else: + return False + def has_warnings(self, pattern: str | None = None) -> bool: """ Check if build produced warnings. diff --git a/tests/doc_test/with_toc/index.rst b/tests/doc_test/with_toc/index.rst index 0e3d3cf..3d38ed3 100644 --- a/tests/doc_test/with_toc/index.rst +++ b/tests/doc_test/with_toc/index.rst @@ -1,13 +1,6 @@ Document with TOC ================= -.. toctree:: - :maxdepth: 2 - :caption: Contents: - - chapter1 - chapter2 - Main Content ------------ @@ -17,3 +10,11 @@ Introduction ~~~~~~~~~~~~ This is the introduction section. + + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + chapter1 + chapter2 diff --git a/tests/doc_test/with_toc_and_typo/_static/custom.css b/tests/doc_test/with_toc_and_typo/_static/custom.css new file mode 100644 index 0000000..867208c --- /dev/null +++ b/tests/doc_test/with_toc_and_typo/_static/custom.css @@ -0,0 +1,20 @@ +div.body h1{ + display: none; +} + +h2 { + page-break-before: avoid !important; + break-before:avoid !important; +} + +span.anchor-before-heading { + display: block; + page-break-before: always; + height: 0; + margin: 0; + padding: 0; +} + +span.anchor-before-heading.first { + page-break-before: avoid !important; +} diff --git a/tests/doc_test/with_toc_and_typo/chapter1.rst b/tests/doc_test/with_toc_and_typo/chapter1.rst new file mode 100644 index 0000000..7c08e48 --- /dev/null +++ b/tests/doc_test/with_toc_and_typo/chapter1.rst @@ -0,0 +1,17 @@ +Chapter 1: Getting Started +========================== + +This is the first chapter of the test document. + +Section 1.1 +----------- + +Content for section 1.1. + +Section 1.2 +----------- + +Content for section 1.2. + +.. note:: + This is a note in chapter 1. diff --git a/tests/doc_test/with_toc_and_typo/chapter2.rst b/tests/doc_test/with_toc_and_typo/chapter2.rst new file mode 100644 index 0000000..1747e20 --- /dev/null +++ b/tests/doc_test/with_toc_and_typo/chapter2.rst @@ -0,0 +1,17 @@ +Chapter 2: Advanced Topcs +========================= + +This is the second chapter. + +Section 2.1 +----------- + +Advanced content here. + +Section 2.2 +----------- + +More advanced topics. + +.. warning:: + This is a warning in chapter 2. diff --git a/tests/doc_test/with_toc_and_typo/conf.py b/tests/doc_test/with_toc_and_typo/conf.py new file mode 100644 index 0000000..611c8f2 --- /dev/null +++ b/tests/doc_test/with_toc_and_typo/conf.py @@ -0,0 +1,11 @@ +"""Configuration with table of contents.""" + +project = "TOCTest" +extensions = ["sphinx_simplepdf"] +master_doc = "index" +exclude_patterns = ["_build"] + +simplepdf_theme = "simplepdf_theme" +simplepdf_toc = True +html_css_files = ["custom.css"] +html_static_path = ["_static"] diff --git a/tests/doc_test/with_toc_and_typo/index.rst b/tests/doc_test/with_toc_and_typo/index.rst new file mode 100644 index 0000000..3d38ed3 --- /dev/null +++ b/tests/doc_test/with_toc_and_typo/index.rst @@ -0,0 +1,20 @@ +Document with TOC +================= + +Main Content +------------ + +This document has a table of contents that links to multiple chapters. + +Introduction +~~~~~~~~~~~~ + +This is the introduction section. + + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + chapter1 + chapter2 diff --git a/tests/imgcompare.py b/tests/imgcompare.py new file mode 100644 index 0000000..1637fd9 --- /dev/null +++ b/tests/imgcompare.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +from pathlib import Path + +import cv2 +import fitz # PyMuPDF +import numpy as np +from skimage.metrics import structural_similarity as ssim + + +# ----------------------------- +# PDF -> PNG-Seiten mit PyMuPDF +# ----------------------------- +def pdf_to_pngs( + pdf_path: str | Path, + out_dir: str | Path, + dpi: int = 300, +) -> list[Path]: + """ + Rendert alle Seiten eines PDFs als PNG-Dateien mittels PyMuPDF. + + Gibt eine sortierte Liste der erzeugten PNG-Pfade zurück. + """ + pdf_path = Path(pdf_path) + out_dir = Path(out_dir) + out_dir.mkdir(parents=True, exist_ok=True) + + doc = fitz.open(str(pdf_path)) + images: list[Path] = [] + + # Zoom-Faktor so wählen, dass ungefähr gewünschtes DPI erreicht wird. + # Standard-PDF-Resolution ~72 dpi → zoom = dpi / 72 + zoom = dpi / 72.0 + mat = fitz.Matrix(zoom, zoom) + + for page_index in range(doc.page_count): + page = doc.load_page(page_index) + pix = page.get_pixmap(matrix=mat, alpha=False) + out_path = out_dir / f"{pdf_path.stem}_{page_index + 1:03d}.png" + # direkt als PNG speichern + pix.pil_save(str(out_path), format="PNG", optimize=False) + images.append(out_path) + + doc.close() + return images + + +# ----------------------------- +# Bildvergleich (SSIM auf Grau) +# ----------------------------- +def load_gray(path: str | Path) -> np.ndarray: + img = cv2.imread(str(path), cv2.IMREAD_GRAYSCALE) + if img is None: + raise FileNotFoundError(path) + return img + + +def normalize_size(a: np.ndarray, b: np.ndarray) -> tuple[np.ndarray, np.ndarray]: + if a.shape == b.shape: + return a, b + h = min(a.shape[0], b.shape[0]) + w = min(a.shape[1], b.shape[1]) + return a[:h, :w], b[:h, :w] + + +def compare_images_ssim( + ref_path: str | Path, + test_path: str | Path, + blur_sigma: int = 1, + diff_threshold: int = 25, +): + ref = load_gray(ref_path) + test = load_gray(test_path) + + ref, test = normalize_size(ref, test) + + if blur_sigma > 0: + k = 2 * blur_sigma + 1 + ref_blur = cv2.GaussianBlur(ref, (k, k), 0) + test_blur = cv2.GaussianBlur(test, (k, k), 0) + else: + ref_blur, test_blur = ref, test + + score, ssim_map = ssim( + ref_blur, + test_blur, + data_range=255, + full=True, + ) + + abs_diff = cv2.absdiff(ref_blur, test_blur) + _, diff_mask = cv2.threshold(abs_diff, diff_threshold, 255, cv2.THRESH_BINARY) + + kernel = np.ones((3, 3), np.uint8) + diff_mask_clean = cv2.morphologyEx(diff_mask, cv2.MORPH_OPEN, kernel, iterations=1) + + changed_pixels = int(np.count_nonzero(diff_mask_clean)) + total_pixels = int(diff_mask_clean.size) + changed_ratio = changed_pixels / total_pixels + + return { + "ssim": float(score), + "changed_ratio": float(changed_ratio), + "changed_pixels": changed_pixels, + "total_pixels": total_pixels, + "diff_mask": diff_mask_clean, + "abs_diff": abs_diff, + "ssim_map": ssim_map, + } + + +def save_debug_images( + out_dir: str | Path, + diff_mask: np.ndarray, + abs_diff: np.ndarray, + ssim_map: np.ndarray, +): + out_dir = Path(out_dir) + out_dir.mkdir(parents=True, exist_ok=True) + + ssim_norm = (ssim_map * 255).astype(np.uint8) + cv2.imwrite(str(out_dir / "diff_mask.png"), diff_mask) + cv2.imwrite(str(out_dir / "abs_diff.png"), abs_diff) + cv2.imwrite(str(out_dir / "ssim_map.png"), ssim_norm) + + +# ----------------------------- +# Gesamtworkflow: PDFs -> PNG -> Vergleich +# ----------------------------- +def compare_pdfs( + ref_pdf: str | Path, + test_pdf: str | Path, + work_dir: str | Path = "artifacts", + dpi: int = 300, + ssim_threshold: float = 0.99, + changed_ratio_threshold: float = 0.001, +) -> bool: + """ + Vergleicht zwei PDFs seitenweise. + + Rückgabewert: True, wenn alle Seiten innerhalb der Toleranzen liegen. + """ + work_dir = Path(work_dir) + ref_dir = work_dir / "ref" + test_dir = work_dir / "test" + + print(f"[INFO] Render reference PDF: {ref_pdf}") + ref_pages = pdf_to_pngs(ref_pdf, ref_dir, dpi=dpi) + print(f"[INFO] Render test PDF: {test_pdf}") + test_pages = pdf_to_pngs(test_pdf, test_dir, dpi=dpi) + + if len(ref_pages) != len(test_pages): + print(f"[ERROR] Page count mismatch: ref={len(ref_pages)}, test={len(test_pages)}") + return False + + all_ok = True + + for ref_img, test_img in zip(ref_pages, test_pages, strict=True): + page_name = ref_img.name + print(f"[INFO] Compare page: {page_name}") + + result = compare_images_ssim(ref_img, test_img) + + s = result["ssim"] + r = result["changed_ratio"] + print(f" SSIM : {s:.6f}") + print(f" Changed ratio : {r * 100:.6f}%") + + is_ok = s > ssim_threshold and r < changed_ratio_threshold + print(f" RESULT : {'OK' if is_ok else 'MISMATCH'}") + + if not is_ok: + all_ok = False + debug_dir = work_dir / f"debug_{ref_img.stem}" + save_debug_images(debug_dir, result["diff_mask"], result["abs_diff"], result["ssim_map"]) + print(f" Debug images : {debug_dir}") + + return all_ok + + +if __name__ == "__main__": + import sys + + if len(sys.argv) != 3: + print(f"Usage: {sys.argv[0]} REF.pdf TEST.pdf") + sys.exit(1) + + ref_pdf = sys.argv[1] + test_pdf = sys.argv[2] + + ok = compare_pdfs(ref_pdf, test_pdf) + sys.exit(0 if ok else 2) diff --git a/tests/ref_pdfs/with_toc/TOCTest.pdf b/tests/ref_pdfs/with_toc/TOCTest.pdf new file mode 100644 index 0000000000000000000000000000000000000000..6574c3167e308150e402747cfc1882fd819781c6 GIT binary patch literal 60355 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%0AnS`xgaIHnd1|t6PZ(CgY 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 5000 -def test_pdf_with_toc(sphinx_build, capsys): +def test_pdf_with_toc(sphinx_build, capsys, refdir): """Test that PDF with table of contents is generated.""" # result = sphinx_build(srcdir="with_toc").build() result = build_and_capture_stdout(sphinx_build, capsys, srcdir="with_toc", build_kwargs={"debug": True}) @@ -72,15 +72,67 @@ def test_pdf_with_toc(sphinx_build, capsys): 3 -4 +3 + +3 4 +""" + in text + ) + + assert result.compare_pdf(refdir / "with_toc" / "TOCTest.pdf") + + +def test_pdf_with_toc_and_typo(sphinx_build, capsys, refdir): + """Test that PDF with table of contents is generated. PDF has typo, image comparison should fail""" + # result = sphinx_build(srcdir="with_toc").build() + result = build_and_capture_stdout(sphinx_build, capsys, srcdir="with_toc_and_typo", build_kwargs={"debug": True}) + + assert result.pdf_exists() + pdf_path = result.pdf_path() + + # Check for specific WeasyPrint anchor warnings + anchor_warnings = result.get_warnings_matching(r"(anchor|link|reference)") + assert len(anchor_warnings) == 0 + + assert page_count(pdf_path) == 5 + + text = extract_pdf_text(pdf_path) + + assert ( + """ +Table of Contents + +Contents: + +Chapter 1: Getting Started + +• Section 1.1 +• Section 1.2 + +Chapter 2: Advanced Topcs + +• Section 2.1 +• Section 2.2 + +3 + +3 + +3 + +3 + +3 4 """ in text ) + assert not result.compare_pdf(refdir / "with_toc" / "TOCTest.pdf", changed_ratio_threshold=0.0001) + @pytest.mark.parametrize("page_format", ["A4", "Letter", "A5"]) def test_pdf_different_page_formats(sphinx_build, capsys, page_format): diff --git a/uv.lock b/uv.lock index 43805a2..debcd84 100644 --- a/uv.lock +++ b/uv.lock @@ -663,6 +663,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] +[[package]] +name = "imageio" +version = "2.37.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/6f/606be632e37bf8d05b253e8626c2291d74c691ddc7bcdf7d6aaf33b32f6a/imageio-2.37.2.tar.gz", hash = "sha256:0212ef2727ac9caa5ca4b2c75ae89454312f440a756fcfc8ef1993e718f50f8a", size = 389600, upload-time = "2025-11-04T14:29:39.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/fe/301e0936b79bcab4cacc7548bf2853fc28dced0a578bab1f7ef53c9aa75b/imageio-2.37.2-py3-none-any.whl", hash = "sha256:ad9adfb20335d718c03de457358ed69f141021a333c40a53e57273d8a5bd0b9b", size = 317646, upload-time = "2025-11-04T14:29:37.948Z" }, +] + [[package]] name = "imagesize" version = "1.4.1" @@ -712,6 +726,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/34/0f/9693e268eb55d13bf9624f2914a5f09b6a75ee086db554d1495074f116de/jsonschema_rs-0.37.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:10fd978a145a6f8d11373879e7d0ff232b409f77c7faf608e6b4549a7f90aaed", size = 2224657, upload-time = "2025-11-30T21:00:21.769Z" }, ] +[[package]] +name = "lazy-loader" +version = "0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/6b/c875b30a1ba490860c93da4cabf479e03f584eba06fe5963f6f6644653d8/lazy_loader-0.4.tar.gz", hash = "sha256:47c75182589b91a4e1a85a136c074285a5ad4d9f39c63e0d7fb76391c4574cd1", size = 15431, upload-time = "2024-04-05T13:03:12.261Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/60/d497a310bde3f01cb805196ac61b7ad6dc5dcf8dce66634dc34364b20b4f/lazy_loader-0.4-py3-none-any.whl", hash = "sha256:342aa8e14d543a154047afb4ba8ef17f5563baad3fc610d7b15b213b0f119efc", size = 12097, upload-time = "2024-04-05T13:03:10.514Z" }, +] + [[package]] name = "libsass" version = "0.23.0" @@ -810,6 +836,32 @@ wheels = [ { 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 = "networkx" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + [[package]] name = "nodeenv" version = "1.10.0" @@ -819,6 +871,174 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, ] +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651, upload-time = "2026-01-31T23:13:10.135Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/44/71852273146957899753e69986246d6a176061ea183407e95418c2aa4d9a/numpy-2.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7e88598032542bd49af7c4747541422884219056c268823ef6e5e89851c8825", size = 16955478, upload-time = "2026-01-31T23:10:25.623Z" }, + { url = "https://files.pythonhosted.org/packages/74/41/5d17d4058bd0cd96bcbd4d9ff0fb2e21f52702aab9a72e4a594efa18692f/numpy-2.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7edc794af8b36ca37ef5fcb5e0d128c7e0595c7b96a2318d1badb6fcd8ee86b1", size = 14965467, upload-time = "2026-01-31T23:10:28.186Z" }, + { url = "https://files.pythonhosted.org/packages/49/48/fb1ce8136c19452ed15f033f8aee91d5defe515094e330ce368a0647846f/numpy-2.4.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:6e9f61981ace1360e42737e2bae58b27bf28a1b27e781721047d84bd754d32e7", size = 5475172, upload-time = "2026-01-31T23:10:30.848Z" }, + { url = "https://files.pythonhosted.org/packages/40/a9/3feb49f17bbd1300dd2570432961f5c8a4ffeff1db6f02c7273bd020a4c9/numpy-2.4.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cb7bbb88aa74908950d979eeaa24dbdf1a865e3c7e45ff0121d8f70387b55f73", size = 6805145, upload-time = "2026-01-31T23:10:32.352Z" }, + { url = "https://files.pythonhosted.org/packages/3f/39/fdf35cbd6d6e2fcad42fcf85ac04a85a0d0fbfbf34b30721c98d602fd70a/numpy-2.4.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f069069931240b3fc703f1e23df63443dbd6390614c8c44a87d96cd0ec81eb1", size = 15966084, upload-time = "2026-01-31T23:10:34.502Z" }, + { url = "https://files.pythonhosted.org/packages/1b/46/6fa4ea94f1ddf969b2ee941290cca6f1bfac92b53c76ae5f44afe17ceb69/numpy-2.4.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c02ef4401a506fb60b411467ad501e1429a3487abca4664871d9ae0b46c8ba32", size = 16899477, upload-time = "2026-01-31T23:10:37.075Z" }, + { url = "https://files.pythonhosted.org/packages/09/a1/2a424e162b1a14a5bd860a464ab4e07513916a64ab1683fae262f735ccd2/numpy-2.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2653de5c24910e49c2b106499803124dde62a5a1fe0eedeaecf4309a5f639390", size = 17323429, upload-time = "2026-01-31T23:10:39.704Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a2/73014149ff250628df72c58204822ac01d768697913881aacf839ff78680/numpy-2.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1ae241bbfc6ae276f94a170b14785e561cb5e7f626b6688cf076af4110887413", size = 18635109, upload-time = "2026-01-31T23:10:41.924Z" }, + { url = "https://files.pythonhosted.org/packages/6c/0c/73e8be2f1accd56df74abc1c5e18527822067dced5ec0861b5bb882c2ce0/numpy-2.4.2-cp311-cp311-win32.whl", hash = "sha256:df1b10187212b198dd45fa943d8985a3c8cf854aed4923796e0e019e113a1bda", size = 6237915, upload-time = "2026-01-31T23:10:45.26Z" }, + { url = "https://files.pythonhosted.org/packages/76/ae/e0265e0163cf127c24c3969d29f1c4c64551a1e375d95a13d32eab25d364/numpy-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:b9c618d56a29c9cb1c4da979e9899be7578d2e0b3c24d52079c166324c9e8695", size = 12607972, upload-time = "2026-01-31T23:10:47.021Z" }, + { url = "https://files.pythonhosted.org/packages/29/a5/c43029af9b8014d6ea157f192652c50042e8911f4300f8f6ed3336bf437f/numpy-2.4.2-cp311-cp311-win_arm64.whl", hash = "sha256:47c5a6ed21d9452b10227e5e8a0e1c22979811cad7dcc19d8e3e2fb8fa03f1a3", size = 10485763, upload-time = "2026-01-31T23:10:50.087Z" }, + { url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963, upload-time = "2026-01-31T23:10:52.147Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571, upload-time = "2026-01-31T23:10:54.789Z" }, + { url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469, upload-time = "2026-01-31T23:10:57.343Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820, upload-time = "2026-01-31T23:10:59.429Z" }, + { url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067, upload-time = "2026-01-31T23:11:01.291Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782, upload-time = "2026-01-31T23:11:03.669Z" }, + { url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128, upload-time = "2026-01-31T23:11:05.913Z" }, + { url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324, upload-time = "2026-01-31T23:11:08.248Z" }, + { url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282, upload-time = "2026-01-31T23:11:10.497Z" }, + { url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210, upload-time = "2026-01-31T23:11:12.176Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171, upload-time = "2026-01-31T23:11:14.684Z" }, + { url = "https://files.pythonhosted.org/packages/a1/22/815b9fe25d1d7ae7d492152adbc7226d3eff731dffc38fe970589fcaaa38/numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c", size = 16663696, upload-time = "2026-01-31T23:11:17.516Z" }, + { url = "https://files.pythonhosted.org/packages/09/f0/817d03a03f93ba9c6c8993de509277d84e69f9453601915e4a69554102a1/numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979", size = 14688322, upload-time = "2026-01-31T23:11:19.883Z" }, + { url = "https://files.pythonhosted.org/packages/da/b4/f805ab79293c728b9a99438775ce51885fd4f31b76178767cfc718701a39/numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98", size = 5198157, upload-time = "2026-01-31T23:11:22.375Z" }, + { url = "https://files.pythonhosted.org/packages/74/09/826e4289844eccdcd64aac27d13b0fd3f32039915dd5b9ba01baae1f436c/numpy-2.4.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef", size = 6546330, upload-time = "2026-01-31T23:11:23.958Z" }, + { url = "https://files.pythonhosted.org/packages/19/fb/cbfdbfa3057a10aea5422c558ac57538e6acc87ec1669e666d32ac198da7/numpy-2.4.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7", size = 15660968, upload-time = "2026-01-31T23:11:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/04/dc/46066ce18d01645541f0186877377b9371b8fa8017fa8262002b4ef22612/numpy-2.4.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499", size = 16607311, upload-time = "2026-01-31T23:11:28.117Z" }, + { url = "https://files.pythonhosted.org/packages/14/d9/4b5adfc39a43fa6bf918c6d544bc60c05236cc2f6339847fc5b35e6cb5b0/numpy-2.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb", size = 17012850, upload-time = "2026-01-31T23:11:30.888Z" }, + { url = "https://files.pythonhosted.org/packages/b7/20/adb6e6adde6d0130046e6fdfb7675cc62bc2f6b7b02239a09eb58435753d/numpy-2.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7", size = 18334210, upload-time = "2026-01-31T23:11:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/78/0e/0a73b3dff26803a8c02baa76398015ea2a5434d9b8265a7898a6028c1591/numpy-2.4.2-cp313-cp313-win32.whl", hash = "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110", size = 5958199, upload-time = "2026-01-31T23:11:35.385Z" }, + { url = "https://files.pythonhosted.org/packages/43/bc/6352f343522fcb2c04dbaf94cb30cca6fd32c1a750c06ad6231b4293708c/numpy-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622", size = 12310848, upload-time = "2026-01-31T23:11:38.001Z" }, + { url = "https://files.pythonhosted.org/packages/6e/8d/6da186483e308da5da1cc6918ce913dcfe14ffde98e710bfeff2a6158d4e/numpy-2.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71", size = 10221082, upload-time = "2026-01-31T23:11:40.392Z" }, + { url = "https://files.pythonhosted.org/packages/25/a1/9510aa43555b44781968935c7548a8926274f815de42ad3997e9e83680dd/numpy-2.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262", size = 14815866, upload-time = "2026-01-31T23:11:42.495Z" }, + { url = "https://files.pythonhosted.org/packages/36/30/6bbb5e76631a5ae46e7923dd16ca9d3f1c93cfa8d4ed79a129814a9d8db3/numpy-2.4.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913", size = 5325631, upload-time = "2026-01-31T23:11:44.7Z" }, + { url = "https://files.pythonhosted.org/packages/46/00/3a490938800c1923b567b3a15cd17896e68052e2145d8662aaf3e1ffc58f/numpy-2.4.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab", size = 6646254, upload-time = "2026-01-31T23:11:46.341Z" }, + { url = "https://files.pythonhosted.org/packages/d3/e9/fac0890149898a9b609caa5af7455a948b544746e4b8fe7c212c8edd71f8/numpy-2.4.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82", size = 15720138, upload-time = "2026-01-31T23:11:48.082Z" }, + { url = "https://files.pythonhosted.org/packages/ea/5c/08887c54e68e1e28df53709f1893ce92932cc6f01f7c3d4dc952f61ffd4e/numpy-2.4.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f", size = 16655398, upload-time = "2026-01-31T23:11:50.293Z" }, + { url = "https://files.pythonhosted.org/packages/4d/89/253db0fa0e66e9129c745e4ef25631dc37d5f1314dad2b53e907b8538e6d/numpy-2.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554", size = 17079064, upload-time = "2026-01-31T23:11:52.927Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d5/cbade46ce97c59c6c3da525e8d95b7abe8a42974a1dc5c1d489c10433e88/numpy-2.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257", size = 18379680, upload-time = "2026-01-31T23:11:55.22Z" }, + { url = "https://files.pythonhosted.org/packages/40/62/48f99ae172a4b63d981babe683685030e8a3df4f246c893ea5c6ef99f018/numpy-2.4.2-cp313-cp313t-win32.whl", hash = "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657", size = 6082433, upload-time = "2026-01-31T23:11:58.096Z" }, + { url = "https://files.pythonhosted.org/packages/07/38/e054a61cfe48ad9f1ed0d188e78b7e26859d0b60ef21cd9de4897cdb5326/numpy-2.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b", size = 12451181, upload-time = "2026-01-31T23:11:59.782Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a4/a05c3a6418575e185dd84d0b9680b6bb2e2dc3e4202f036b7b4e22d6e9dc/numpy-2.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1", size = 10290756, upload-time = "2026-01-31T23:12:02.438Z" }, + { url = "https://files.pythonhosted.org/packages/18/88/b7df6050bf18fdcfb7046286c6535cabbdd2064a3440fca3f069d319c16e/numpy-2.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:444be170853f1f9d528428eceb55f12918e4fda5d8805480f36a002f1415e09b", size = 16663092, upload-time = "2026-01-31T23:12:04.521Z" }, + { url = "https://files.pythonhosted.org/packages/25/7a/1fee4329abc705a469a4afe6e69b1ef7e915117747886327104a8493a955/numpy-2.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1240d50adff70c2a88217698ca844723068533f3f5c5fa6ee2e3220e3bdb000", size = 14698770, upload-time = "2026-01-31T23:12:06.96Z" }, + { url = "https://files.pythonhosted.org/packages/fb/0b/f9e49ba6c923678ad5bc38181c08ac5e53b7a5754dbca8e581aa1a56b1ff/numpy-2.4.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:7cdde6de52fb6664b00b056341265441192d1291c130e99183ec0d4b110ff8b1", size = 5208562, upload-time = "2026-01-31T23:12:09.632Z" }, + { url = "https://files.pythonhosted.org/packages/7d/12/d7de8f6f53f9bb76997e5e4c069eda2051e3fe134e9181671c4391677bb2/numpy-2.4.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:cda077c2e5b780200b6b3e09d0b42205a3d1c68f30c6dceb90401c13bff8fe74", size = 6543710, upload-time = "2026-01-31T23:12:11.969Z" }, + { url = "https://files.pythonhosted.org/packages/09/63/c66418c2e0268a31a4cf8a8b512685748200f8e8e8ec6c507ce14e773529/numpy-2.4.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30291931c915b2ab5717c2974bb95ee891a1cf22ebc16a8006bd59cd210d40a", size = 15677205, upload-time = "2026-01-31T23:12:14.33Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6c/7f237821c9642fb2a04d2f1e88b4295677144ca93285fd76eff3bcba858d/numpy-2.4.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bba37bc29d4d85761deed3954a1bc62be7cf462b9510b51d367b769a8c8df325", size = 16611738, upload-time = "2026-01-31T23:12:16.525Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a7/39c4cdda9f019b609b5c473899d87abff092fc908cfe4d1ecb2fcff453b0/numpy-2.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b2f0073ed0868db1dcd86e052d37279eef185b9c8db5bf61f30f46adac63c909", size = 17028888, upload-time = "2026-01-31T23:12:19.306Z" }, + { url = "https://files.pythonhosted.org/packages/da/b3/e84bb64bdfea967cc10950d71090ec2d84b49bc691df0025dddb7c26e8e3/numpy-2.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f54844851cdb630ceb623dcec4db3240d1ac13d4990532446761baede94996a", size = 18339556, upload-time = "2026-01-31T23:12:21.816Z" }, + { url = "https://files.pythonhosted.org/packages/88/f5/954a291bc1192a27081706862ac62bb5920fbecfbaa302f64682aa90beed/numpy-2.4.2-cp314-cp314-win32.whl", hash = "sha256:12e26134a0331d8dbd9351620f037ec470b7c75929cb8a1537f6bfe411152a1a", size = 6006899, upload-time = "2026-01-31T23:12:24.14Z" }, + { url = "https://files.pythonhosted.org/packages/05/cb/eff72a91b2efdd1bc98b3b8759f6a1654aa87612fc86e3d87d6fe4f948c4/numpy-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:068cdb2d0d644cdb45670810894f6a0600797a69c05f1ac478e8d31670b8ee75", size = 12443072, upload-time = "2026-01-31T23:12:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/37/75/62726948db36a56428fce4ba80a115716dc4fad6a3a4352487f8bb950966/numpy-2.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:6ed0be1ee58eef41231a5c943d7d1375f093142702d5723ca2eb07db9b934b05", size = 10494886, upload-time = "2026-01-31T23:12:28.488Z" }, + { url = "https://files.pythonhosted.org/packages/36/2f/ee93744f1e0661dc267e4b21940870cabfae187c092e1433b77b09b50ac4/numpy-2.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98f16a80e917003a12c0580f97b5f875853ebc33e2eaa4bccfc8201ac6869308", size = 14818567, upload-time = "2026-01-31T23:12:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/a7/24/6535212add7d76ff938d8bdc654f53f88d35cddedf807a599e180dcb8e66/numpy-2.4.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:20abd069b9cda45874498b245c8015b18ace6de8546bf50dfa8cea1696ed06ef", size = 5328372, upload-time = "2026-01-31T23:12:32.962Z" }, + { url = "https://files.pythonhosted.org/packages/5e/9d/c48f0a035725f925634bf6b8994253b43f2047f6778a54147d7e213bc5a7/numpy-2.4.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e98c97502435b53741540a5717a6749ac2ada901056c7db951d33e11c885cc7d", size = 6649306, upload-time = "2026-01-31T23:12:34.797Z" }, + { url = "https://files.pythonhosted.org/packages/81/05/7c73a9574cd4a53a25907bad38b59ac83919c0ddc8234ec157f344d57d9a/numpy-2.4.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da6cad4e82cb893db4b69105c604d805e0c3ce11501a55b5e9f9083b47d2ffe8", size = 15722394, upload-time = "2026-01-31T23:12:36.565Z" }, + { url = "https://files.pythonhosted.org/packages/35/fa/4de10089f21fc7d18442c4a767ab156b25c2a6eaf187c0db6d9ecdaeb43f/numpy-2.4.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4424677ce4b47fe73c8b5556d876571f7c6945d264201180db2dc34f676ab5", size = 16653343, upload-time = "2026-01-31T23:12:39.188Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f9/d33e4ffc857f3763a57aa85650f2e82486832d7492280ac21ba9efda80da/numpy-2.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2b8f157c8a6f20eb657e240f8985cc135598b2b46985c5bccbde7616dc9c6b1e", size = 17078045, upload-time = "2026-01-31T23:12:42.041Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b8/54bdb43b6225badbea6389fa038c4ef868c44f5890f95dd530a218706da3/numpy-2.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5daf6f3914a733336dab21a05cdec343144600e964d2fcdabaac0c0269874b2a", size = 18380024, upload-time = "2026-01-31T23:12:44.331Z" }, + { url = "https://files.pythonhosted.org/packages/a5/55/6e1a61ded7af8df04016d81b5b02daa59f2ea9252ee0397cb9f631efe9e5/numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443", size = 6153937, upload-time = "2026-01-31T23:12:47.229Z" }, + { url = "https://files.pythonhosted.org/packages/45/aa/fa6118d1ed6d776b0983f3ceac9b1a5558e80df9365b1c3aa6d42bf9eee4/numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236", size = 12631844, upload-time = "2026-01-31T23:12:48.997Z" }, + { url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379, upload-time = "2026-01-31T23:12:51.345Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f8/50e14d36d915ef64d8f8bc4a087fc8264d82c785eda6711f80ab7e620335/numpy-2.4.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:89f7268c009bc492f506abd6f5265defa7cb3f7487dc21d357c3d290add45082", size = 16833179, upload-time = "2026-01-31T23:12:53.5Z" }, + { url = "https://files.pythonhosted.org/packages/17/17/809b5cad63812058a8189e91a1e2d55a5a18fd04611dbad244e8aeae465c/numpy-2.4.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6dee3bb76aa4009d5a912180bf5b2de012532998d094acee25d9cb8dee3e44a", size = 14889755, upload-time = "2026-01-31T23:12:55.933Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ea/181b9bcf7627fc8371720316c24db888dcb9829b1c0270abf3d288b2e29b/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:cd2bd2bbed13e213d6b55dc1d035a4f91748a7d3edc9480c13898b0353708920", size = 5399500, upload-time = "2026-01-31T23:12:58.671Z" }, + { url = "https://files.pythonhosted.org/packages/33/9f/413adf3fc955541ff5536b78fcf0754680b3c6d95103230252a2c9408d23/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:cf28c0c1d4c4bf00f509fa7eb02c58d7caf221b50b467bcb0d9bbf1584d5c821", size = 6714252, upload-time = "2026-01-31T23:13:00.518Z" }, + { url = "https://files.pythonhosted.org/packages/91/da/643aad274e29ccbdf42ecd94dafe524b81c87bcb56b83872d54827f10543/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e04ae107ac591763a47398bb45b568fc38f02dbc4aa44c063f67a131f99346cb", size = 15797142, upload-time = "2026-01-31T23:13:02.219Z" }, + { url = "https://files.pythonhosted.org/packages/66/27/965b8525e9cb5dc16481b30a1b3c21e50c7ebf6e9dbd48d0c4d0d5089c7e/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:602f65afdef699cda27ec0b9224ae5dc43e328f4c24c689deaf77133dbee74d0", size = 16727979, upload-time = "2026-01-31T23:13:04.62Z" }, + { url = "https://files.pythonhosted.org/packages/de/e5/b7d20451657664b07986c2f6e3be564433f5dcaf3482d68eaecd79afaf03/numpy-2.4.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be71bf1edb48ebbbf7f6337b5bfd2f895d1902f6335a5830b20141fc126ffba0", size = 12502577, upload-time = "2026-01-31T23:13:07.08Z" }, +] + +[[package]] +name = "opencv-python" +version = "4.13.0.92" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/6f/5a28fef4c4a382be06afe3938c64cc168223016fa520c5abaf37e8862aa5/opencv_python-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:caf60c071ec391ba51ed00a4a920f996d0b64e3e46068aac1f646b5de0326a19", size = 46247052, upload-time = "2026-02-05T07:01:25.046Z" }, + { url = "https://files.pythonhosted.org/packages/08/ac/6c98c44c650b8114a0fb901691351cfb3956d502e8e9b5cd27f4ee7fbf2f/opencv_python-4.13.0.92-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:5868a8c028a0b37561579bfb8ac1875babdc69546d236249fff296a8c010ccf9", size = 32568781, upload-time = "2026-02-05T07:01:41.379Z" }, + { url = "https://files.pythonhosted.org/packages/3e/51/82fed528b45173bf629fa44effb76dff8bc9f4eeaee759038362dfa60237/opencv_python-4.13.0.92-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bc2596e68f972ca452d80f444bc404e08807d021fbba40df26b61b18e01838a", size = 47685527, upload-time = "2026-02-05T06:59:11.24Z" }, + { url = "https://files.pythonhosted.org/packages/db/07/90b34a8e2cf9c50fe8ed25cac9011cde0676b4d9d9c973751ac7616223a2/opencv_python-4.13.0.92-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:402033cddf9d294693094de5ef532339f14ce821da3ad7df7c9f6e8316da32cf", size = 70460872, upload-time = "2026-02-05T06:59:19.162Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/7a9cc719b3eaf4377b9c2e3edeb7ed3a81de41f96421510c0a169ca3cfd4/opencv_python-4.13.0.92-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:bccaabf9eb7f897ca61880ce2869dcd9b25b72129c28478e7f2a5e8dee945616", size = 46708208, upload-time = "2026-02-05T06:59:15.419Z" }, + { url = "https://files.pythonhosted.org/packages/fd/55/b3b49a1b97aabcfbbd6c7326df9cb0b6fa0c0aefa8e89d500939e04aa229/opencv_python-4.13.0.92-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:620d602b8f7d8b8dab5f4b99c6eb353e78d3fb8b0f53db1bd258bb1aa001c1d5", size = 72927042, upload-time = "2026-02-05T06:59:23.389Z" }, + { url = "https://files.pythonhosted.org/packages/fb/17/de5458312bcb07ddf434d7bfcb24bb52c59635ad58c6e7c751b48949b009/opencv_python-4.13.0.92-cp37-abi3-win32.whl", hash = "sha256:372fe164a3148ac1ca51e5f3ad0541a4a276452273f503441d718fab9c5e5f59", size = 30932638, upload-time = "2026-02-05T07:02:14.98Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a5/1be1516390333ff9be3a9cb648c9f33df79d5096e5884b5df71a588af463/opencv_python-4.13.0.92-cp37-abi3-win_amd64.whl", hash = "sha256:423d934c9fafb91aad38edf26efb46da91ffbc05f3f59c4b0c72e699720706f5", size = 40212062, upload-time = "2026-02-05T07:02:12.724Z" }, +] + [[package]] name = "packaging" version = "26.0" @@ -1146,6 +1366,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] +[[package]] +name = "pymupdf" +version = "1.27.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/0c/40dda0cc4bd2220a2ef75f8c53dd7d8ed1e29681fcb3df75db6ee9677a7e/pymupdf-1.27.1.tar.gz", hash = "sha256:4afbde0769c336717a149ab0de3330dcb75378f795c1a8c5af55c1a628b17d55", size = 85303479, upload-time = "2026-02-12T08:29:17.682Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/19/fde6ea4712a904b65e8f41124a0e4233879b87a770fe6a8ce857964de6d5/pymupdf-1.27.1-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:bee9f95512f9556dbf2cacfd1413c61b29a55baa07fa7f8fc83d221d8419888a", size = 23986707, upload-time = "2026-02-11T15:03:24.025Z" }, + { url = "https://files.pythonhosted.org/packages/75/c2/070dff91ad3f1bc16fd6c6ceff23495601fcce4c92d28be534417596418a/pymupdf-1.27.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:3de95a0889395b0966fafd11b94980b7543a816e89dd1c218597a08543ac3415", size = 23263493, upload-time = "2026-02-11T15:03:45.528Z" }, + { url = "https://files.pythonhosted.org/packages/8e/db/937377f4b3e0fbf6273c17436a49f7db17df1a46b1be9e26653b6fafc0e1/pymupdf-1.27.1-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2c9d9353b840040cbc724341f4095fb7e2cc1a12a9147d0ec1a0a79f5d773147", size = 24317651, upload-time = "2026-02-11T22:33:38.967Z" }, + { url = "https://files.pythonhosted.org/packages/72/d5/c701cf2d0cdd6e5d6bca3ca9188d7f5d7ce3ae67dd1368d658cd4bae2707/pymupdf-1.27.1-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:aeaed76e72cbc061149a825ab0811c5f4752970c56591c2938c5042ec06b26e1", size = 24945742, upload-time = "2026-02-11T15:04:06.21Z" }, + { url = "https://files.pythonhosted.org/packages/2b/29/690202b38b93cf77b73a29c25a63a2b6f3fcb36b1f75006e50b8dee7c108/pymupdf-1.27.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4f1837554134fb45d390a44de8844b2ca9b6c901c82ccc90b340e3b7f3b126ca", size = 25167965, upload-time = "2026-02-11T22:36:35.478Z" }, + { url = "https://files.pythonhosted.org/packages/8a/81/f937e6aa606fd263c3a45d0ff0f0bbdbf3fb779933091fc0f6179513cc93/pymupdf-1.27.1-cp310-abi3-win32.whl", hash = "sha256:fa33b512d82c6c4852edadf57f22d5f27d16243bb33dac0fbe4eb0f281c5b17e", size = 18006253, upload-time = "2026-02-12T13:48:07.129Z" }, + { url = "https://files.pythonhosted.org/packages/3e/99/fe4a7752990bf65277718fffbead4478de9afd1c7288d7a6d643f79a6fa7/pymupdf-1.27.1-cp310-abi3-win_amd64.whl", hash = "sha256:4b6268dff3a9d713034eba5c2ffce0da37c62443578941ac5df433adcde57b2f", size = 19236703, upload-time = "2026-02-11T15:04:19.607Z" }, +] + [[package]] name = "pyphen" version = "0.17.2" @@ -1330,6 +1565,254 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" }, ] +[[package]] +name = "scikit-image" +version = "0.25.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "imageio", marker = "python_full_version < '3.11'" }, + { name = "lazy-loader", marker = "python_full_version < '3.11'" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "packaging", marker = "python_full_version < '3.11'" }, + { name = "pillow", marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "tifffile", version = "2025.5.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/a8/3c0f256012b93dd2cb6fda9245e9f4bff7dc0486880b248005f15ea2255e/scikit_image-0.25.2.tar.gz", hash = "sha256:e5a37e6cd4d0c018a7a55b9d601357e3382826d3888c10d0213fc63bff977dde", size = 22693594, upload-time = "2025-02-18T18:05:24.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/cb/016c63f16065c2d333c8ed0337e18a5cdf9bc32d402e4f26b0db362eb0e2/scikit_image-0.25.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d3278f586793176599df6a4cf48cb6beadae35c31e58dc01a98023af3dc31c78", size = 13988922, upload-time = "2025-02-18T18:04:11.069Z" }, + { url = "https://files.pythonhosted.org/packages/30/ca/ff4731289cbed63c94a0c9a5b672976603118de78ed21910d9060c82e859/scikit_image-0.25.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:5c311069899ce757d7dbf1d03e32acb38bb06153236ae77fcd820fd62044c063", size = 13192698, upload-time = "2025-02-18T18:04:15.362Z" }, + { url = "https://files.pythonhosted.org/packages/39/6d/a2aadb1be6d8e149199bb9b540ccde9e9622826e1ab42fe01de4c35ab918/scikit_image-0.25.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be455aa7039a6afa54e84f9e38293733a2622b8c2fb3362b822d459cc5605e99", size = 14153634, upload-time = "2025-02-18T18:04:18.496Z" }, + { url = "https://files.pythonhosted.org/packages/96/08/916e7d9ee4721031b2f625db54b11d8379bd51707afaa3e5a29aecf10bc4/scikit_image-0.25.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4c464b90e978d137330be433df4e76d92ad3c5f46a22f159520ce0fdbea8a09", size = 14767545, upload-time = "2025-02-18T18:04:22.556Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ee/c53a009e3997dda9d285402f19226fbd17b5b3cb215da391c4ed084a1424/scikit_image-0.25.2-cp310-cp310-win_amd64.whl", hash = "sha256:60516257c5a2d2f74387c502aa2f15a0ef3498fbeaa749f730ab18f0a40fd054", size = 12812908, upload-time = "2025-02-18T18:04:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/c4/97/3051c68b782ee3f1fb7f8f5bb7d535cf8cb92e8aae18fa9c1cdf7e15150d/scikit_image-0.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f4bac9196fb80d37567316581c6060763b0f4893d3aca34a9ede3825bc035b17", size = 14003057, upload-time = "2025-02-18T18:04:30.395Z" }, + { url = "https://files.pythonhosted.org/packages/19/23/257fc696c562639826065514d551b7b9b969520bd902c3a8e2fcff5b9e17/scikit_image-0.25.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:d989d64ff92e0c6c0f2018c7495a5b20e2451839299a018e0e5108b2680f71e0", size = 13180335, upload-time = "2025-02-18T18:04:33.449Z" }, + { url = "https://files.pythonhosted.org/packages/ef/14/0c4a02cb27ca8b1e836886b9ec7c9149de03053650e9e2ed0625f248dd92/scikit_image-0.25.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2cfc96b27afe9a05bc92f8c6235321d3a66499995675b27415e0d0c76625173", size = 14144783, upload-time = "2025-02-18T18:04:36.594Z" }, + { url = "https://files.pythonhosted.org/packages/dd/9b/9fb556463a34d9842491d72a421942c8baff4281025859c84fcdb5e7e602/scikit_image-0.25.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24cc986e1f4187a12aa319f777b36008764e856e5013666a4a83f8df083c2641", size = 14785376, upload-time = "2025-02-18T18:04:39.856Z" }, + { url = "https://files.pythonhosted.org/packages/de/ec/b57c500ee85885df5f2188f8bb70398481393a69de44a00d6f1d055f103c/scikit_image-0.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:b4f6b61fc2db6340696afe3db6b26e0356911529f5f6aee8c322aa5157490c9b", size = 12791698, upload-time = "2025-02-18T18:04:42.868Z" }, + { url = "https://files.pythonhosted.org/packages/35/8c/5df82881284459f6eec796a5ac2a0a304bb3384eec2e73f35cfdfcfbf20c/scikit_image-0.25.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8db8dd03663112783221bf01ccfc9512d1cc50ac9b5b0fe8f4023967564719fb", size = 13986000, upload-time = "2025-02-18T18:04:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e6/93bebe1abcdce9513ffec01d8af02528b4c41fb3c1e46336d70b9ed4ef0d/scikit_image-0.25.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:483bd8cc10c3d8a7a37fae36dfa5b21e239bd4ee121d91cad1f81bba10cfb0ed", size = 13235893, upload-time = "2025-02-18T18:04:51.049Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/eda616e33f67129e5979a9eb33c710013caa3aa8a921991e6cc0b22cea33/scikit_image-0.25.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d1e80107bcf2bf1291acfc0bf0425dceb8890abe9f38d8e94e23497cbf7ee0d", size = 14178389, upload-time = "2025-02-18T18:04:54.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b5/b75527c0f9532dd8a93e8e7cd8e62e547b9f207d4c11e24f0006e8646b36/scikit_image-0.25.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a17e17eb8562660cc0d31bb55643a4da996a81944b82c54805c91b3fe66f4824", size = 15003435, upload-time = "2025-02-18T18:04:57.586Z" }, + { url = "https://files.pythonhosted.org/packages/34/e3/49beb08ebccda3c21e871b607c1cb2f258c3fa0d2f609fed0a5ba741b92d/scikit_image-0.25.2-cp312-cp312-win_amd64.whl", hash = "sha256:bdd2b8c1de0849964dbc54037f36b4e9420157e67e45a8709a80d727f52c7da2", size = 12899474, upload-time = "2025-02-18T18:05:01.166Z" }, + { url = "https://files.pythonhosted.org/packages/e6/7c/9814dd1c637f7a0e44342985a76f95a55dd04be60154247679fd96c7169f/scikit_image-0.25.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7efa888130f6c548ec0439b1a7ed7295bc10105458a421e9bf739b457730b6da", size = 13921841, upload-time = "2025-02-18T18:05:03.963Z" }, + { url = "https://files.pythonhosted.org/packages/84/06/66a2e7661d6f526740c309e9717d3bd07b473661d5cdddef4dd978edab25/scikit_image-0.25.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:dd8011efe69c3641920614d550f5505f83658fe33581e49bed86feab43a180fc", size = 13196862, upload-time = "2025-02-18T18:05:06.986Z" }, + { url = "https://files.pythonhosted.org/packages/4e/63/3368902ed79305f74c2ca8c297dfeb4307269cbe6402412668e322837143/scikit_image-0.25.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28182a9d3e2ce3c2e251383bdda68f8d88d9fff1a3ebe1eb61206595c9773341", size = 14117785, upload-time = "2025-02-18T18:05:10.69Z" }, + { url = "https://files.pythonhosted.org/packages/cd/9b/c3da56a145f52cd61a68b8465d6a29d9503bc45bc993bb45e84371c97d94/scikit_image-0.25.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8abd3c805ce6944b941cfed0406d88faeb19bab3ed3d4b50187af55cf24d147", size = 14977119, upload-time = "2025-02-18T18:05:13.871Z" }, + { url = "https://files.pythonhosted.org/packages/8a/97/5fcf332e1753831abb99a2525180d3fb0d70918d461ebda9873f66dcc12f/scikit_image-0.25.2-cp313-cp313-win_amd64.whl", hash = "sha256:64785a8acefee460ec49a354706db0b09d1f325674107d7fa3eadb663fb56d6f", size = 12885116, upload-time = "2025-02-18T18:05:17.844Z" }, + { url = "https://files.pythonhosted.org/packages/10/cc/75e9f17e3670b5ed93c32456fda823333c6279b144cd93e2c03aa06aa472/scikit_image-0.25.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:330d061bd107d12f8d68f1d611ae27b3b813b8cdb0300a71d07b1379178dd4cd", size = 13862801, upload-time = "2025-02-18T18:05:20.783Z" }, +] + +[[package]] +name = "scikit-image" +version = "0.26.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "imageio", marker = "python_full_version >= '3.11'" }, + { name = "lazy-loader", marker = "python_full_version >= '3.11'" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging", marker = "python_full_version >= '3.11'" }, + { name = "pillow", marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "tifffile", version = "2026.2.24", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/b4/2528bb43c67d48053a7a649a9666432dc307d66ba02e3a6d5c40f46655df/scikit_image-0.26.0.tar.gz", hash = "sha256:f5f970ab04efad85c24714321fcc91613fcb64ef2a892a13167df2f3e59199fa", size = 22729739, upload-time = "2025-12-20T17:12:21.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/16/8a407688b607f86f81f8c649bf0d68a2a6d67375f18c2d660aba20f5b648/scikit_image-0.26.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b1ede33a0fb3731457eaf53af6361e73dd510f449dac437ab54573b26788baf0", size = 12355510, upload-time = "2025-12-20T17:10:31.628Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f9/7efc088ececb6f6868fd4475e16cfafc11f242ce9ab5fc3557d78b5da0d4/scikit_image-0.26.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7af7aa331c6846bd03fa28b164c18d0c3fd419dbb888fb05e958ac4257a78fdd", size = 12056334, upload-time = "2025-12-20T17:10:34.559Z" }, + { url = "https://files.pythonhosted.org/packages/9f/1e/bc7fb91fb5ff65ef42346c8b7ee8b09b04eabf89235ab7dbfdfd96cbd1ea/scikit_image-0.26.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ea6207d9e9d21c3f464efe733121c0504e494dbdc7728649ff3e23c3c5a4953", size = 13297768, upload-time = "2025-12-20T17:10:37.733Z" }, + { url = "https://files.pythonhosted.org/packages/a5/2a/e71c1a7d90e70da67b88ccc609bd6ae54798d5847369b15d3a8052232f9d/scikit_image-0.26.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74aa5518ccea28121f57a95374581d3b979839adc25bb03f289b1bc9b99c58af", size = 13711217, upload-time = "2025-12-20T17:10:40.935Z" }, + { url = "https://files.pythonhosted.org/packages/d4/59/9637ee12c23726266b91296791465218973ce1ad3e4c56fc81e4d8e7d6e1/scikit_image-0.26.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d5c244656de905e195a904e36dbc18585e06ecf67d90f0482cbde63d7f9ad59d", size = 14337782, upload-time = "2025-12-20T17:10:43.452Z" }, + { url = "https://files.pythonhosted.org/packages/e7/5c/a3e1e0860f9294663f540c117e4bf83d55e5b47c281d475cc06227e88411/scikit_image-0.26.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:21a818ee6ca2f2131b9e04d8eb7637b5c18773ebe7b399ad23dcc5afaa226d2d", size = 14805997, upload-time = "2025-12-20T17:10:45.93Z" }, + { url = "https://files.pythonhosted.org/packages/d3/c6/2eeacf173da041a9e388975f54e5c49df750757fcfc3ee293cdbbae1ea0a/scikit_image-0.26.0-cp311-cp311-win_amd64.whl", hash = "sha256:9490360c8d3f9a7e85c8de87daf7c0c66507960cf4947bb9610d1751928721c7", size = 11878486, upload-time = "2025-12-20T17:10:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a4/a852c4949b9058d585e762a66bf7e9a2cd3be4795cd940413dfbfbb0ce79/scikit_image-0.26.0-cp311-cp311-win_arm64.whl", hash = "sha256:0baa0108d2d027f34d748e84e592b78acc23e965a5de0e4bb03cf371de5c0581", size = 11346518, upload-time = "2025-12-20T17:10:50.575Z" }, + { url = "https://files.pythonhosted.org/packages/99/e8/e13757982264b33a1621628f86b587e9a73a13f5256dad49b19ba7dc9083/scikit_image-0.26.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d454b93a6fa770ac5ae2d33570f8e7a321bb80d29511ce4b6b78058ebe176e8c", size = 12376452, upload-time = "2025-12-20T17:10:52.796Z" }, + { url = "https://files.pythonhosted.org/packages/e3/be/f8dd17d0510f9911f9f17ba301f7455328bf13dae416560126d428de9568/scikit_image-0.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3409e89d66eff5734cd2b672d1c48d2759360057e714e1d92a11df82c87cba37", size = 12061567, upload-time = "2025-12-20T17:10:55.207Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/c70120a6880579fb42b91567ad79feb4772f7be72e8d52fec403a3dde0c6/scikit_image-0.26.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c717490cec9e276afb0438dd165b7c3072d6c416709cc0f9f5a4c1070d23a44", size = 13084214, upload-time = "2025-12-20T17:10:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/f4/a2/70401a107d6d7466d64b466927e6b96fcefa99d57494b972608e2f8be50f/scikit_image-0.26.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7df650e79031634ac90b11e64a9eedaf5a5e06fcd09bcd03a34be01745744466", size = 13561683, upload-time = "2025-12-20T17:10:59.49Z" }, + { url = "https://files.pythonhosted.org/packages/13/a5/48bdfd92794c5002d664e0910a349d0a1504671ef5ad358150f21643c79a/scikit_image-0.26.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cefd85033e66d4ea35b525bb0937d7f42d4cdcfed2d1888e1570d5ce450d3932", size = 14112147, upload-time = "2025-12-20T17:11:02.083Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b5/ac71694da92f5def5953ca99f18a10fe98eac2dd0a34079389b70b4d0394/scikit_image-0.26.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3f5bf622d7c0435884e1e141ebbe4b2804e16b2dd23ae4c6183e2ea99233be70", size = 14661625, upload-time = "2025-12-20T17:11:04.528Z" }, + { url = "https://files.pythonhosted.org/packages/23/4d/a3cc1e96f080e253dad2251bfae7587cf2b7912bcd76fd43fd366ff35a87/scikit_image-0.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:abed017474593cd3056ae0fe948d07d0747b27a085e92df5474f4955dd65aec0", size = 11911059, upload-time = "2025-12-20T17:11:06.61Z" }, + { url = "https://files.pythonhosted.org/packages/35/8a/d1b8055f584acc937478abf4550d122936f420352422a1a625eef2c605d8/scikit_image-0.26.0-cp312-cp312-win_arm64.whl", hash = "sha256:4d57e39ef67a95d26860c8caf9b14b8fb130f83b34c6656a77f191fa6d1d04d8", size = 11348740, upload-time = "2025-12-20T17:11:09.118Z" }, + { url = "https://files.pythonhosted.org/packages/4f/48/02357ffb2cca35640f33f2cfe054a4d6d5d7a229b88880a64f1e45c11f4e/scikit_image-0.26.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a2e852eccf41d2d322b8e60144e124802873a92b8d43a6f96331aa42888491c7", size = 12346329, upload-time = "2025-12-20T17:11:11.599Z" }, + { url = "https://files.pythonhosted.org/packages/67/b9/b792c577cea2c1e94cda83b135a656924fc57c428e8a6d302cd69aac1b60/scikit_image-0.26.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:98329aab3bc87db352b9887f64ce8cdb8e75f7c2daa19927f2e121b797b678d5", size = 12031726, upload-time = "2025-12-20T17:11:13.871Z" }, + { url = "https://files.pythonhosted.org/packages/07/a9/9564250dfd65cb20404a611016db52afc6268b2b371cd19c7538ea47580f/scikit_image-0.26.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:915bb3ba66455cf8adac00dc8fdf18a4cd29656aec7ddd38cb4dda90289a6f21", size = 13094910, upload-time = "2025-12-20T17:11:16.2Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b8/0d8eeb5a9fd7d34ba84f8a55753a0a3e2b5b51b2a5a0ade648a8db4a62f7/scikit_image-0.26.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b36ab5e778bf50af5ff386c3ac508027dc3aaeccf2161bdf96bde6848f44d21b", size = 13660939, upload-time = "2025-12-20T17:11:18.464Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d6/91d8973584d4793d4c1a847d388e34ef1218d835eeddecfc9108d735b467/scikit_image-0.26.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:09bad6a5d5949c7896c8347424c4cca899f1d11668030e5548813ab9c2865dcb", size = 14138938, upload-time = "2025-12-20T17:11:20.919Z" }, + { url = "https://files.pythonhosted.org/packages/39/9a/7e15d8dc10d6bbf212195fb39bdeb7f226c46dd53f9c63c312e111e2e175/scikit_image-0.26.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:aeb14db1ed09ad4bee4ceb9e635547a8d5f3549be67fc6c768c7f923e027e6cd", size = 14752243, upload-time = "2025-12-20T17:11:23.347Z" }, + { url = "https://files.pythonhosted.org/packages/8f/58/2b11b933097bc427e42b4a8b15f7de8f24f2bac1fd2779d2aea1431b2c31/scikit_image-0.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:ac529eb9dbd5954f9aaa2e3fe9a3fd9661bfe24e134c688587d811a0233127f1", size = 11906770, upload-time = "2025-12-20T17:11:25.297Z" }, + { url = "https://files.pythonhosted.org/packages/ad/ec/96941474a18a04b69b6f6562a5bd79bd68049fa3728d3b350976eccb8b93/scikit_image-0.26.0-cp313-cp313-win_arm64.whl", hash = "sha256:a2d211bc355f59725efdcae699b93b30348a19416cc9e017f7b2fb599faf7219", size = 11342506, upload-time = "2025-12-20T17:11:27.399Z" }, + { url = "https://files.pythonhosted.org/packages/03/e5/c1a9962b0cf1952f42d32b4a2e48eed520320dbc4d2ff0b981c6fa508b6b/scikit_image-0.26.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9eefb4adad066da408a7601c4c24b07af3b472d90e08c3e7483d4e9e829d8c49", size = 12663278, upload-time = "2025-12-20T17:11:29.358Z" }, + { url = "https://files.pythonhosted.org/packages/ae/97/c1a276a59ce8e4e24482d65c1a3940d69c6b3873279193b7ebd04e5ee56b/scikit_image-0.26.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6caec76e16c970c528d15d1c757363334d5cb3069f9cea93d2bead31820511f3", size = 12405142, upload-time = "2025-12-20T17:11:31.282Z" }, + { url = "https://files.pythonhosted.org/packages/d4/4a/f1cbd1357caef6c7993f7efd514d6e53d8fd6f7fe01c4714d51614c53289/scikit_image-0.26.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a07200fe09b9d99fcdab959859fe0f7db8df6333d6204344425d476850ce3604", size = 12942086, upload-time = "2025-12-20T17:11:33.683Z" }, + { url = "https://files.pythonhosted.org/packages/5b/6f/74d9fb87c5655bd64cf00b0c44dc3d6206d9002e5f6ba1c9aeb13236f6bf/scikit_image-0.26.0-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92242351bccf391fc5df2d1529d15470019496d2498d615beb68da85fe7fdf37", size = 13265667, upload-time = "2025-12-20T17:11:36.11Z" }, + { url = "https://files.pythonhosted.org/packages/a7/73/faddc2413ae98d863f6fa2e3e14da4467dd38e788e1c23346cf1a2b06b97/scikit_image-0.26.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:52c496f75a7e45844d951557f13c08c81487c6a1da2e3c9c8a39fcde958e02cc", size = 14001966, upload-time = "2025-12-20T17:11:38.55Z" }, + { url = "https://files.pythonhosted.org/packages/02/94/9f46966fa042b5d57c8cd641045372b4e0df0047dd400e77ea9952674110/scikit_image-0.26.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:20ef4a155e2e78b8ab973998e04d8a361d49d719e65412405f4dadd9155a61d9", size = 14359526, upload-time = "2025-12-20T17:11:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b4/2840fe38f10057f40b1c9f8fb98a187a370936bf144a4ac23452c5ef1baf/scikit_image-0.26.0-cp313-cp313t-win_amd64.whl", hash = "sha256:c9087cf7d0e7f33ab5c46d2068d86d785e70b05400a891f73a13400f1e1faf6a", size = 12287629, upload-time = "2025-12-20T17:11:43.11Z" }, + { url = "https://files.pythonhosted.org/packages/22/ba/73b6ca70796e71f83ab222690e35a79612f0117e5aaf167151b7d46f5f2c/scikit_image-0.26.0-cp313-cp313t-win_arm64.whl", hash = "sha256:27d58bc8b2acd351f972c6508c1b557cfed80299826080a4d803dd29c51b707e", size = 11647755, upload-time = "2025-12-20T17:11:45.279Z" }, + { url = "https://files.pythonhosted.org/packages/51/44/6b744f92b37ae2833fd423cce8f806d2368859ec325a699dc30389e090b9/scikit_image-0.26.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:63af3d3a26125f796f01052052f86806da5b5e54c6abef152edb752683075a9c", size = 12365810, upload-time = "2025-12-20T17:11:47.357Z" }, + { url = "https://files.pythonhosted.org/packages/40/f5/83590d9355191f86ac663420fec741b82cc547a4afe7c4c1d986bf46e4db/scikit_image-0.26.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ce00600cd70d4562ed59f80523e18cdcc1fae0e10676498a01f73c255774aefd", size = 12075717, upload-time = "2025-12-20T17:11:49.483Z" }, + { url = "https://files.pythonhosted.org/packages/72/48/253e7cf5aee6190459fe136c614e2cbccc562deceb4af96e0863f1b8ee29/scikit_image-0.26.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6381edf972b32e4f54085449afde64365a57316637496c1325a736987083e2ab", size = 13161520, upload-time = "2025-12-20T17:11:51.58Z" }, + { url = "https://files.pythonhosted.org/packages/73/c3/cec6a3cbaadfdcc02bd6ff02f3abfe09eaa7f4d4e0a525a1e3a3f4bce49c/scikit_image-0.26.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6624a76c6085218248154cc7e1500e6b488edcd9499004dd0d35040607d7505", size = 13684340, upload-time = "2025-12-20T17:11:53.708Z" }, + { url = "https://files.pythonhosted.org/packages/d4/0d/39a776f675d24164b3a267aa0db9f677a4cb20127660d8bf4fd7fef66817/scikit_image-0.26.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f775f0e420faac9c2aa6757135f4eb468fb7b70e0b67fa77a5e79be3c30ee331", size = 14203839, upload-time = "2025-12-20T17:11:55.89Z" }, + { url = "https://files.pythonhosted.org/packages/ee/25/2514df226bbcedfe9b2caafa1ba7bc87231a0c339066981b182b08340e06/scikit_image-0.26.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede4d6d255cc5da9faeb2f9ba7fedbc990abbc652db429f40a16b22e770bb578", size = 14770021, upload-time = "2025-12-20T17:11:58.014Z" }, + { url = "https://files.pythonhosted.org/packages/8d/5b/0671dc91c0c79340c3fe202f0549c7d3681eb7640fe34ab68a5f090a7c7f/scikit_image-0.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:0660b83968c15293fd9135e8d860053ee19500d52bf55ca4fb09de595a1af650", size = 12023490, upload-time = "2025-12-20T17:12:00.013Z" }, + { url = "https://files.pythonhosted.org/packages/65/08/7c4cb59f91721f3de07719085212a0b3962e3e3f2d1818cbac4eeb1ea53e/scikit_image-0.26.0-cp314-cp314-win_arm64.whl", hash = "sha256:b8d14d3181c21c11170477a42542c1addc7072a90b986675a71266ad17abc37f", size = 11473782, upload-time = "2025-12-20T17:12:01.983Z" }, + { url = "https://files.pythonhosted.org/packages/49/41/65c4258137acef3d73cb561ac55512eacd7b30bb4f4a11474cad526bc5db/scikit_image-0.26.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:cde0bbd57e6795eba83cb10f71a677f7239271121dc950bc060482834a668ad1", size = 12686060, upload-time = "2025-12-20T17:12:03.886Z" }, + { url = "https://files.pythonhosted.org/packages/e7/32/76971f8727b87f1420a962406388a50e26667c31756126444baf6668f559/scikit_image-0.26.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:163e9afb5b879562b9aeda0dd45208a35316f26cc7a3aed54fd601604e5cf46f", size = 12422628, upload-time = "2025-12-20T17:12:05.921Z" }, + { url = "https://files.pythonhosted.org/packages/37/0d/996febd39f757c40ee7b01cdb861867327e5c8e5f595a634e8201462d958/scikit_image-0.26.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724f79fd9b6cb6f4a37864fe09f81f9f5d5b9646b6868109e1b100d1a7019e59", size = 12962369, upload-time = "2025-12-20T17:12:07.912Z" }, + { url = "https://files.pythonhosted.org/packages/48/b4/612d354f946c9600e7dea012723c11d47e8d455384e530f6daaaeb9bf62c/scikit_image-0.26.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3268f13310e6857508bd87202620df996199a016a1d281b309441d227c822394", size = 13272431, upload-time = "2025-12-20T17:12:10.255Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6e/26c00b466e06055a086de2c6e2145fe189ccdc9a1d11ccc7de020f2591ad/scikit_image-0.26.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fac96a1f9b06cd771cbbb3cd96c5332f36d4efd839b1d8b053f79e5887acde62", size = 14016362, upload-time = "2025-12-20T17:12:12.793Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/00a90402e1775634043c2a0af8a3c76ad450866d9fa444efcc43b553ba2d/scikit_image-0.26.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c1e7bd342f43e7a97e571b3f03ba4c1293ea1a35c3f13f41efdc8a81c1dc8f2", size = 14364151, upload-time = "2025-12-20T17:12:14.909Z" }, + { url = "https://files.pythonhosted.org/packages/da/ca/918d8d306bd43beacff3b835c6d96fac0ae64c0857092f068b88db531a7c/scikit_image-0.26.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b702c3bb115e1dcf4abf5297429b5c90f2189655888cbed14921f3d26f81d3a4", size = 12413484, upload-time = "2025-12-20T17:12:17.046Z" }, + { url = "https://files.pythonhosted.org/packages/dc/cd/4da01329b5a8d47ff7ec3c99a2b02465a8017b186027590dc7425cee0b56/scikit_image-0.26.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0608aa4a9ec39e0843de10d60edb2785a30c1c47819b67866dd223ebd149acaf", size = 11769501, upload-time = "2025-12-20T17:12:19.339Z" }, +] + +[[package]] +name = "scipy" +version = "1.15.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770, upload-time = "2025-05-08T16:04:20.849Z" }, + { url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511, upload-time = "2025-05-08T16:04:27.103Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151, upload-time = "2025-05-08T16:04:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732, upload-time = "2025-05-08T16:04:36.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617, upload-time = "2025-05-08T16:04:43.546Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964, upload-time = "2025-05-08T16:04:49.431Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749, upload-time = "2025-05-08T16:04:55.215Z" }, + { url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383, upload-time = "2025-05-08T16:05:01.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201, upload-time = "2025-05-08T16:05:08.166Z" }, + { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255, upload-time = "2025-05-08T16:05:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035, upload-time = "2025-05-08T16:05:20.152Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499, upload-time = "2025-05-08T16:05:24.494Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602, upload-time = "2025-05-08T16:05:29.313Z" }, + { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415, upload-time = "2025-05-08T16:05:34.699Z" }, + { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622, upload-time = "2025-05-08T16:05:40.762Z" }, + { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796, upload-time = "2025-05-08T16:05:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684, upload-time = "2025-05-08T16:05:54.22Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504, upload-time = "2025-05-08T16:06:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735, upload-time = "2025-05-08T16:06:06.471Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284, upload-time = "2025-05-08T16:06:11.686Z" }, + { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958, upload-time = "2025-05-08T16:06:15.97Z" }, + { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454, upload-time = "2025-05-08T16:06:20.394Z" }, + { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199, upload-time = "2025-05-08T16:06:26.159Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455, upload-time = "2025-05-08T16:06:32.778Z" }, + { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140, upload-time = "2025-05-08T16:06:39.249Z" }, + { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload-time = "2025-05-08T16:06:45.729Z" }, + { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload-time = "2025-05-08T16:06:52.623Z" }, + { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256, upload-time = "2025-05-08T16:06:58.696Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540, upload-time = "2025-05-08T16:07:04.209Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115, upload-time = "2025-05-08T16:07:08.998Z" }, + { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884, upload-time = "2025-05-08T16:07:14.091Z" }, + { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018, upload-time = "2025-05-08T16:07:19.427Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716, upload-time = "2025-05-08T16:07:25.712Z" }, + { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342, upload-time = "2025-05-08T16:07:31.468Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869, upload-time = "2025-05-08T16:07:38.002Z" }, + { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851, upload-time = "2025-05-08T16:08:33.671Z" }, + { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011, upload-time = "2025-05-08T16:07:44.039Z" }, + { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407, upload-time = "2025-05-08T16:07:49.891Z" }, + { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030, upload-time = "2025-05-08T16:07:54.121Z" }, + { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709, upload-time = "2025-05-08T16:07:58.506Z" }, + { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045, upload-time = "2025-05-08T16:08:03.929Z" }, + { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062, upload-time = "2025-05-08T16:08:09.558Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132, upload-time = "2025-05-08T16:08:15.34Z" }, + { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503, upload-time = "2025-05-08T16:08:21.513Z" }, + { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097, upload-time = "2025-05-08T16:08:27.627Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/75/b4ce781849931fef6fd529afa6b63711d5a733065722d0c3e2724af9e40a/scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec", size = 31613675, upload-time = "2026-02-23T00:16:00.13Z" }, + { url = "https://files.pythonhosted.org/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696", size = 28162057, upload-time = "2026-02-23T00:16:09.456Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee", size = 20334032, upload-time = "2026-02-23T00:16:17.358Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd", size = 22709533, upload-time = "2026-02-23T00:16:25.791Z" }, + { url = "https://files.pythonhosted.org/packages/4d/60/8804678875fc59362b0fb759ab3ecce1f09c10a735680318ac30da8cd76b/scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c", size = 33062057, upload-time = "2026-02-23T00:16:36.931Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4", size = 35349300, upload-time = "2026-02-23T00:16:49.108Z" }, + { url = "https://files.pythonhosted.org/packages/b4/3d/7ccbbdcbb54c8fdc20d3b6930137c782a163fa626f0aef920349873421ba/scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444", size = 35127333, upload-time = "2026-02-23T00:17:01.293Z" }, + { url = "https://files.pythonhosted.org/packages/e8/19/f926cb11c42b15ba08e3a71e376d816ac08614f769b4f47e06c3580c836a/scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082", size = 37741314, upload-time = "2026-02-23T00:17:12.576Z" }, + { url = "https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff", size = 36607512, upload-time = "2026-02-23T00:17:23.424Z" }, + { url = "https://files.pythonhosted.org/packages/68/7f/bdd79ceaad24b671543ffe0ef61ed8e659440eb683b66f033454dcee90eb/scipy-1.17.1-cp311-cp311-win_arm64.whl", hash = "sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d", size = 24599248, upload-time = "2026-02-23T00:17:34.561Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, + { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, + { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, + { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" }, + { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, + { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, + { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, + { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, + { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, + { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, +] + [[package]] name = "snowballstemmer" version = "3.0.1" @@ -1490,11 +1973,15 @@ demo = [ { name = "sphinxcontrib-plantuml" }, ] dev = [ + { name = "opencv-python" }, { name = "pdfminer-six" }, + { name = "pymupdf" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-xdist" }, { name = "ruff" }, + { name = "scikit-image", version = "0.25.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scikit-image", version = "0.26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "tox" }, { name = "tox-uv" }, ] @@ -1516,7 +2003,7 @@ requires-dist = [ { name = "pre-commit", specifier = ">=3.5" }, { name = "pytest", specifier = ">=7.0" }, { name = "sphinx", specifier = ">=7.0" }, - { name = "weasyprint", specifier = ">=67.0" }, + { name = "weasyprint", specifier = "==67.0" }, ] [package.metadata.requires-dev] @@ -1527,11 +2014,14 @@ demo = [ { name = "sphinxcontrib-plantuml" }, ] dev = [ + { name = "opencv-python", specifier = ">=4.8.0" }, { name = "pdfminer-six", specifier = ">=20220319" }, + { name = "pymupdf", specifier = ">=1.24.0" }, { name = "pytest", specifier = ">=7.0" }, { name = "pytest-cov" }, { name = "pytest-xdist", specifier = ">=3.0" }, { name = "ruff", specifier = ">=0.4.0" }, + { name = "scikit-image", specifier = ">=0.22.0" }, { name = "tox", specifier = ">=4.0" }, { name = "tox-uv", specifier = ">=1.0" }, ] @@ -1619,6 +2109,38 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, ] +[[package]] +name = "tifffile" +version = "2025.5.10" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/d0/18fed0fc0916578a4463f775b0fbd9c5fed2392152d039df2fb533bfdd5d/tifffile-2025.5.10.tar.gz", hash = "sha256:018335d34283aa3fd8c263bae5c3c2b661ebc45548fde31504016fcae7bf1103", size = 365290, upload-time = "2025-05-10T19:22:34.386Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/06/bd0a6097da704a7a7c34a94cfd771c3ea3c2f405dd214e790d22c93f6be1/tifffile-2025.5.10-py3-none-any.whl", hash = "sha256:e37147123c0542d67bc37ba5cdd67e12ea6fbe6e86c52bee037a9eb6a064e5ad", size = 226533, upload-time = "2025-05-10T19:22:27.279Z" }, +] + +[[package]] +name = "tifffile" +version = "2026.2.24" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/1c/19fc653e2b05ec0defae511b03b330ca60c95f2c47fcaaf21c52c6e84aa8/tifffile-2026.2.24.tar.gz", hash = "sha256:d73cfa6d7a8f5775a1e3c9f3bfca77c992946639fb41a5bbe888878cb6964dc6", size = 387373, upload-time = "2026-02-24T23:59:11.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/fe/80250dc06cd4a3a5afe7059875a8d53e97a78528c5dd9ea8c3f981fb897a/tifffile-2026.2.24-py3-none-any.whl", hash = "sha256:38ef6258c2bd8dd3551c7480c6d75a36c041616262e6cd55a50dd16046b71863", size = 243223, upload-time = "2026-02-24T23:59:10.131Z" }, +] + [[package]] name = "tinycss2" version = "1.5.1" @@ -1807,7 +2329,7 @@ wheels = [ [[package]] name = "weasyprint" -version = "68.0" +version = "67.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, @@ -1819,9 +2341,9 @@ dependencies = [ { name = "tinycss2" }, { name = "tinyhtml5" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6f/c8/269c96363db39e34cdb99c7afecaaf8130b7e4c176bff28c74877308e0f3/weasyprint-68.0.tar.gz", hash = "sha256:447f40898b747cb44ac31a5d493d512e7441fd56e13f63744c099383bbf9cda9", size = 1541418, upload-time = "2026-01-19T14:54:45.596Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/bc/79a65b3a406cb62a1982fec8b49134b25a3b31abb094ca493c9fddff5492/weasyprint-67.0.tar.gz", hash = "sha256:fdfbccf700e8086c8fd1607ec42e25d4b584512c29af2d9913587a4e448dead4", size = 1534152, upload-time = "2025-12-02T16:11:36.972Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/5a/c7954167c05ee882a4640b6da0a343c37e3b9de352619c86f8c4efefbb00/weasyprint-68.0-py3-none-any.whl", hash = "sha256:c2cb40c71b50837c5971f00171c9e4078e8c9912dd7c217f3e90e068f11e8aa1", size = 319688, upload-time = "2026-01-19T14:54:44.242Z" }, + { url = "https://files.pythonhosted.org/packages/1e/3a/a225e214ae2accd8781e4d22e9397bd51290c631ea0943d3a0a1840bc667/weasyprint-67.0-py3-none-any.whl", hash = "sha256:abc2f40872ea01c29c11f7799dafc4b23c078335bf7777f72a8affeb36e1d201", size = 316309, upload-time = "2025-12-02T16:11:35.402Z" }, ] [[package]] From 7b960b5671eec14e238e9faab7daa4977c5b7139 Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Sat, 28 Feb 2026 19:00:55 +0100 Subject: [PATCH 25/26] [#83] make test with pdf images independent form use sphinx version --- tests/doc_test/with_toc/conf.py | 1 + tests/doc_test/with_toc_and_typo/conf.py | 1 + tests/ref_pdfs/with_toc/TOCTest.pdf | Bin 60355 -> 59279 bytes 3 files changed, 2 insertions(+) diff --git a/tests/doc_test/with_toc/conf.py b/tests/doc_test/with_toc/conf.py index 611c8f2..ab63e57 100644 --- a/tests/doc_test/with_toc/conf.py +++ b/tests/doc_test/with_toc/conf.py @@ -9,3 +9,4 @@ simplepdf_toc = True html_css_files = ["custom.css"] html_static_path = ["_static"] +html_show_sphinx = False diff --git a/tests/doc_test/with_toc_and_typo/conf.py b/tests/doc_test/with_toc_and_typo/conf.py index 611c8f2..ab63e57 100644 --- a/tests/doc_test/with_toc_and_typo/conf.py +++ b/tests/doc_test/with_toc_and_typo/conf.py @@ -9,3 +9,4 @@ simplepdf_toc = True html_css_files = ["custom.css"] html_static_path = ["_static"] +html_show_sphinx = False diff --git a/tests/ref_pdfs/with_toc/TOCTest.pdf b/tests/ref_pdfs/with_toc/TOCTest.pdf index 6574c3167e308150e402747cfc1882fd819781c6..14a17a7c87d072a2c767077b2423a92a0246e873 100644 GIT binary patch delta 5335 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 delta 6410 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 WWSM5WPmKcx3kqU0G0CdSVgE1B&P-GQ From 1171f1c53a9027ef4f9f04bb8b14c72e7b51bf09 Mon Sep 17 00:00:00 2001 From: Joerg Kreuzberger Date: Sat, 28 Feb 2026 19:25:46 +0100 Subject: [PATCH 26/26] [#83] allow to specify page range as slice for comparison --- tests/conftest.py | 10 +++++++++- tests/imgcompare.py | 32 ++++++++++++++++++++++---------- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index e1b43e7..67b3d08 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -195,13 +195,21 @@ def pdf_exists(self, basename: str | None = None) -> bool: except FileNotFoundError: return False - def compare_pdf(self, ref_pdf: Path, changed_ratio_threshold: float = 0.001) -> bool: + def compare_pdf( + self, + ref_pdf: Path, + pages: slice | None = None, + changed_ratio_threshold: float = 0.001, + ssim_threshold: float = 0.99, + ) -> bool: if self.outdir is not None: return compare_pdfs( ref_pdf=ref_pdf, + pages=pages, test_pdf=self.pdf_path(), work_dir=Path(self.outdir.parent / "png"), changed_ratio_threshold=changed_ratio_threshold, + ssim_threshold=ssim_threshold, ) else: return False diff --git a/tests/imgcompare.py b/tests/imgcompare.py index 1637fd9..05c6cc5 100644 --- a/tests/imgcompare.py +++ b/tests/imgcompare.py @@ -10,7 +10,7 @@ # ----------------------------- -# PDF -> PNG-Seiten mit PyMuPDF +# PDF -> PNG pages with PyMuPDF # ----------------------------- def pdf_to_pngs( pdf_path: str | Path, @@ -18,9 +18,9 @@ def pdf_to_pngs( dpi: int = 300, ) -> list[Path]: """ - Rendert alle Seiten eines PDFs als PNG-Dateien mittels PyMuPDF. + Render all pages of a PDF as PNG files using PyMuPDF. - Gibt eine sortierte Liste der erzeugten PNG-Pfade zurück. + Returns a sorted list of the generated PNG file paths. """ pdf_path = Path(pdf_path) out_dir = Path(out_dir) @@ -29,8 +29,8 @@ def pdf_to_pngs( doc = fitz.open(str(pdf_path)) images: list[Path] = [] - # Zoom-Faktor so wählen, dass ungefähr gewünschtes DPI erreicht wird. - # Standard-PDF-Resolution ~72 dpi → zoom = dpi / 72 + # Choose zoom factor so that the target DPI is approximated. + # Default PDF resolution is ~72 dpi → zoom = dpi / 72 zoom = dpi / 72.0 mat = fitz.Matrix(zoom, zoom) @@ -38,7 +38,7 @@ def pdf_to_pngs( page = doc.load_page(page_index) pix = page.get_pixmap(matrix=mat, alpha=False) out_path = out_dir / f"{pdf_path.stem}_{page_index + 1:03d}.png" - # direkt als PNG speichern + # Save directly as PNG pix.pil_save(str(out_path), format="PNG", optimize=False) images.append(out_path) @@ -47,7 +47,7 @@ def pdf_to_pngs( # ----------------------------- -# Bildvergleich (SSIM auf Grau) +# Image comparison (SSIM on gray) # ----------------------------- def load_gray(path: str | Path) -> np.ndarray: img = cv2.imread(str(path), cv2.IMREAD_GRAYSCALE) @@ -126,7 +126,7 @@ def save_debug_images( # ----------------------------- -# Gesamtworkflow: PDFs -> PNG -> Vergleich +# Full workflow: PDFs -> PNG -> comparison # ----------------------------- def compare_pdfs( ref_pdf: str | Path, @@ -135,11 +135,13 @@ def compare_pdfs( dpi: int = 300, ssim_threshold: float = 0.99, changed_ratio_threshold: float = 0.001, + pages: slice | None = None, ) -> bool: """ - Vergleicht zwei PDFs seitenweise. + Compare two PDFs page by page. - Rückgabewert: True, wenn alle Seiten innerhalb der Toleranzen liegen. + If `pages` is given, only the selected page indices (0-based) are compared. + Example: pages=slice(1, 3) compares pages 1 and 2. """ work_dir = Path(work_dir) ref_dir = work_dir / "ref" @@ -154,8 +156,18 @@ def compare_pdfs( print(f"[ERROR] Page count mismatch: ref={len(ref_pages)}, test={len(test_pages)}") return False + # Apply slice if given (0-based indices) + if pages is not None: + ref_pages = ref_pages[pages] + test_pages = test_pages[pages] + print(f"[INFO] Restricting comparison to pages slice {pages}") + all_ok = True + if len(ref_pages) == 0: + print("[ERROR] No page selected for reference") + return False + for ref_img, test_img in zip(ref_pages, test_pages, strict=True): page_name = ref_img.name print(f"[INFO] Compare page: {page_name}")