diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..5b6f735bf29520a0a5a05ba83d3bd3983f913946 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,50 @@ +# Ref: https://github.com/fastapi/full-stack-fastapi-template/blob/master/backend/Dockerfile +FROM python:3.12-slim-bookworm + +# Print logs immediately +# Ref: https://docs.python.org/3/using/cmdline.html#envvar-PYTHONUNBUFFERED +ENV PYTHONUNBUFFERED=1 + +# Install system dependencies including OpenGL libraries +RUN apt-get update && apt-get install -y \ + libgl1-mesa-glx \ + libglib2.0-0 \ + && rm -rf /var/lib/apt/lists/* + +# Change the working directory to the `app` directory +WORKDIR /app + +# Install uv +# Ref: https://docs.astral.sh/uv/guides/integration/docker/#installing-uv +COPY --from=ghcr.io/astral-sh/uv:0.5.18 /uv /uvx /bin/ + +# Place executables in the environment at the front of the path +# Ref: https://docs.astral.sh/uv/guides/integration/docker/#using-the-environment +ENV PATH="/app/.venv/bin:$PATH" + +# Compile bytecode to speed up the startup time +# Ref: https://docs.astral.sh/uv/guides/integration/docker/#compiling-bytecode +ENV UV_COMPILE_BYTECODE=1 + +# uv Cache +# Ref: https://docs.astral.sh/uv/guides/integration/docker/#caching +ENV UV_LINK_MODE=copy + +# Install dependencies +# Ref: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=bind,source=uv.lock,target=uv.lock \ + --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ + uv sync --frozen --no-install-project + +# Copy the project into the image +COPY . . + +# Sync the project +# Ref: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync --all-extras + +EXPOSE 8501 +# Set the default command +CMD ["streamlit", "run", "src/pdf2u/gui.py"] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..b4b62cbdc2499f53a69cb90455153c8dac61b0fb --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,262 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.version] +path = "src/pdf2u/__init__.py" +# FROM: https://hatch.pypa.io/latest/version/ + +[tool.hatch.build.targets.wheel] +packages = ["src/pdf2u"] +# FROM: https://hatch.pypa.io/latest/build/ + +[project] +name = "pdf2u" +version = "0.0.4" +description = "Yet Another Document Translator" +classifiers = [ + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] # FROM: https://pypi.org/classifiers/ +readme = "README.md" +requires-python = ">=3.10,<3.13" +license = { file = "LICENSE" } +authors = [{ name = "A.J.Zeller", email = "hello@atticux.me" }] +maintainers = [{ name = "A.J.Zeller", email = "hello@atticux.me" }] +# dynamic = ["version"] # https://hatch.pypa.io/latest/config/metadata/#version +dependencies = [ + "bitstring>=4.3.0", + "configargparse>=1.7", + "httpx[socks]>=0.27.0", + "huggingface-hub>=0.27.0", + "numpy>=2.0.2", + "onnx>=1.17.0", + "onnxruntime>=1.16.1", + "openai>=1.59.3", + "opencv-python>=4.10.0.84", + "orjson>=3.10.14", + "pdfminer-six>=20240706", + "peewee>=3.17.8", + "rich>=13.9.4", + "toml>=0.10.2", + "tqdm>=4.67.1", + "xsdata[cli,lxml,soap]>=24.12", + "msgpack>=1.1.0", + "typer>=0.15.1", + "pymupdf==1.24.5", +] + +[project.urls] +Homepage = "https://github.com/atticuszeller/pdf2u" +Issues = "https://github.com/atticuszeller/pdf2u/issues" + +[project.scripts] # build-backend config needed +pdf2u = "pdf2u.main:app" +# FROM: https://packaging.python.org/en/latest/guides/writing-pyproject-toml/ + +[project.optional-dependencies] +gui = ["pypdf2>=3.0.1", "streamlit>=1.42.2", "streamlit-pdf-viewer>=0.0.21"] +# optional deps for package installation + +[dependency-groups] +dev = [ + "ruff>=0.6.3", + "mypy>=1.11.2", + "pre-commit>=3.8.0", + "pytest>=8.3.2", + "pytest-sugar>=1.0.0", + "coverage>=7.6.1", + "git-cliff>=2.6.1", + "bump-my-version>=0.28.0", + "typos>=1.26.8", + "fonttools>=4.56.0", +] + +## Test +[tool.mypy] +strict = true +exclude = ["venv", ".venv"] + +[tool.pytest.ini_options] +# Set additional command line options for pytest +# Ref: https://docs.pytest.org/en/stable/reference/reference.html#command-line-flags +addopts = "-rXs --strict-config --strict-markers --tb=long" +xfail_strict = true # Treat tests that are marked as xfail but pass as test failures +filterwarnings = ["error"] # Treat all warnings as errors +pythonpath = "src/pdf2u/" + +[tool.coverage.run] +branch = true + +[tool.coverage.report] +skip_covered = true +show_missing = true +precision = 2 +exclude_lines = [ + 'def __repr__', + 'pragma= no cover', + 'raise NotImplementedError', + 'if TYPE_CHECKING=', + 'if typing.TYPE_CHECKING=', + '@overload', + '@typing.overload', + '\(Protocol\)=$', + 'typing.assert_never', + 'assert_never', + 'if __name__ == .__main__.=', +] + +## Linter and formatter +[tool.ruff] +# cover and extend the default config in https=//docs.astral.sh/ruff/configuration/ +extend-exclude = [""] +target-version = "py310" + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "UP", # pyupgrade + "ARG001", # unused arguments in functions +] + +isort = { combine-as-imports = true, split-on-trailing-comma = false } + +# Avoid trying to fix flake8-bugbear (`B`) violations. +unfixable = ["B"] + +[tool.ruff.format] +docstring-code-format = true +skip-magic-trailing-comma = true + +# Reference +# 1. https=//github.com/Kludex/python-template/blob/main/template/%7B%7B%20project_slug%20%7D%7D/pyproject.toml.jinja +# 2. https=//github.com/fastapi/full-stack-fastapi-template/blob/master/backend/pyproject.toml +# 3. https=//github.com/pydantic/logfire +# 4. https=//coverage.readthedocs.io/en/latest/index.html + +## VCS +[tool.git-cliff.remote.github] +owner = "atticuszeller" +repo = "python-uv-package" + +[tool.git-cliff.changelog] +# template for the changelog header +header = """ +# Changelog\n +All notable changes to this project will be documented in this file.\n +""" +# template for the changelog body +# https://keats.github.io/tera/docs/#introduction +body = """ +{% if version %}\ + ## {{ version | trim_start_matches(pat="v") }} - {{ timestamp | date(format="%Y-%m-%d") }} +{% else %}\ + ## unreleased +{% endif %}\ +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | striptags | trim | upper_first }} + {% for commit in commits| unique(attribute="message") %} + - {% if commit.scope %}*({{ commit.scope }})* {% endif %}\ + {% if commit.breaking %}[**breaking**] {% endif %}\ + {{ commit.message | upper_first }}\ + {% if commit.remote.pr_number %} in #{{ commit.remote.pr_number }}{%- endif %}\ + {% endfor %} +{% endfor %}\n +""" +# template for the changelog footer +footer = """ + +""" +# remove the leading and trailings +trim = true +# postprocessors +# postprocessors = [ +# { pattern = '', replace = "https://github.com/atticuszeller/python-uv" }, # replace repository URL +# ] +# render body even when there are no releases to process +render_always = true +# output file path +output = "CHANGELOG.md" + +[tool.git-cliff.git] +# parse the commits based on https://www.conventionalcommits.org +conventional_commits = true +# filter out the commits that are not conventional +filter_unconventional = true +# process each line of a commit as an individual commit +split_commits = false +# regex for preprocessing the commit messages +commit_preprocessors = [ + # If the spelling is incorrect, it will be automatically fixed. + { pattern = '.*', replace_command = 'typos --write-changes -' }, +] +# regex for parsing and grouping commits +commit_parsers = [ + { message = "^feat", group = "🚀 Features" }, + { message = "^fix", group = "🐛 Bug Fixes" }, + { message = "^doc", group = "📚 Documentation" }, + { message = "^perf", group = "⚡ Performance" }, + { message = "^refactor", group = "🚜 Refactor" }, + { message = "^style", group = "🎨 Styling" }, + { message = "^test", group = "🧪 Testing" }, + { message = "^chore\\(release\\)", skip = true }, + { message = "^chore\\(deps.*\\)", skip = true }, + { message = "^chore\\(pr\\)", skip = true }, + { message = "^chore\\(pull\\)", skip = true }, + { message = "^chore|^ci", group = "⚙️ Miscellaneous Tasks" }, + { body = ".*security", group = "🛡️ Security" }, + { message = "^revert", group = "◀️ Revert" }, +] +# filter out the commits that are not matched by commit parsers +filter_commits = false +# sort the tags topologically +topo_order = false +# sort the commits inside sections by oldest/newest order +sort_commits = "oldest" + +[tool.bumpversion] +current_version = "0.0.4" +parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)" +serialize = ["{major}.{minor}.{patch}"] +search = "{current_version}" +replace = "{new_version}" +regex = false +ignore_missing_version = false +ignore_missing_files = false +tag = true +sign_tags = false +tag_name = "v{new_version}" +tag_message = "chore(release): {current_version} → {new_version}" +allow_dirty = true # git-cliff first then bump patch +commit = true +message = "chore(release): {current_version} → {new_version}" +commit_args = "" +setup_hooks = [] +pre_commit_hooks = [] +post_commit_hooks = [] + +[[tool.bumpversion.files]] +filename = "src/pdf2u/__init__.py" + +[[tool.bumpversion.files]] +filename = "pyproject.toml" +search = "version = \"{current_version}\"" +replace = "version = \"{new_version}\"" + +[[tool.bumpversion.files]] +filename = "CHANGELOG.md" +search = "unreleased" +replace = "{new_version} - {now:%Y-%m-%d}" + +# https://callowayproject.github.io/bump-my-version/reference/search-and-replace-config/ diff --git a/src/pdf2u/__init__.py b/src/pdf2u/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..81f0fdeccf66f1b74184b96bd53b64be1b903aae --- /dev/null +++ b/src/pdf2u/__init__.py @@ -0,0 +1 @@ +__version__ = "0.0.4" diff --git a/src/pdf2u/__pycache__/__init__.cpython-311.pyc b/src/pdf2u/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..16fe3069d73586e424739878634dc58acca36203 Binary files /dev/null and b/src/pdf2u/__pycache__/__init__.cpython-311.pyc differ diff --git a/src/pdf2u/__pycache__/__init__.cpython-312.pyc b/src/pdf2u/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2ec6ba253182d309247cb6b370ba69600e4b006c Binary files /dev/null and b/src/pdf2u/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/pdf2u/__pycache__/const.cpython-311.pyc b/src/pdf2u/__pycache__/const.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3f277fccdc5805742ddf03266a0ac3e6c9d918f7 Binary files /dev/null and b/src/pdf2u/__pycache__/const.cpython-311.pyc differ diff --git a/src/pdf2u/__pycache__/const.cpython-312.pyc b/src/pdf2u/__pycache__/const.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..651a27d501cde9cc678fc3a7436ead37a8a4decd Binary files /dev/null and b/src/pdf2u/__pycache__/const.cpython-312.pyc differ diff --git a/src/pdf2u/__pycache__/converter.cpython-311.pyc b/src/pdf2u/__pycache__/converter.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6eb8df1af43303613bfdd4fda9c871a666fc3091 Binary files /dev/null and b/src/pdf2u/__pycache__/converter.cpython-311.pyc differ diff --git a/src/pdf2u/__pycache__/converter.cpython-312.pyc b/src/pdf2u/__pycache__/converter.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4733a0e1600ad28ee852e255c57c903e1cf0ca6a Binary files /dev/null and b/src/pdf2u/__pycache__/converter.cpython-312.pyc differ diff --git a/src/pdf2u/__pycache__/high_level.cpython-311.pyc b/src/pdf2u/__pycache__/high_level.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2e35c67218423e271a7eb70aaa8fbc9d9815a78e Binary files /dev/null and b/src/pdf2u/__pycache__/high_level.cpython-311.pyc differ diff --git a/src/pdf2u/__pycache__/high_level.cpython-312.pyc b/src/pdf2u/__pycache__/high_level.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d15107a6a2113bd691304fd95edd2e3adceacd0f Binary files /dev/null and b/src/pdf2u/__pycache__/high_level.cpython-312.pyc differ diff --git a/src/pdf2u/__pycache__/io.cpython-312.pyc b/src/pdf2u/__pycache__/io.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..10e9ce8227384a1017a74a31ba148f0c6208570a Binary files /dev/null and b/src/pdf2u/__pycache__/io.cpython-312.pyc differ diff --git a/src/pdf2u/__pycache__/main.cpython-311.pyc b/src/pdf2u/__pycache__/main.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dd6c45ab1d59a0c4b4246939e6bf1ae597bf13f0 Binary files /dev/null and b/src/pdf2u/__pycache__/main.cpython-311.pyc differ diff --git a/src/pdf2u/__pycache__/main.cpython-312.pyc b/src/pdf2u/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..25dc2bf4adad210ebe397f1fce9005aed5b6333d Binary files /dev/null and b/src/pdf2u/__pycache__/main.cpython-312.pyc differ diff --git a/src/pdf2u/__pycache__/pdfinterp.cpython-311.pyc b/src/pdf2u/__pycache__/pdfinterp.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..499483689160cf06f4d813c772ab10a3d11b54a7 Binary files /dev/null and b/src/pdf2u/__pycache__/pdfinterp.cpython-311.pyc differ diff --git a/src/pdf2u/__pycache__/pdfinterp.cpython-312.pyc b/src/pdf2u/__pycache__/pdfinterp.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ac43fcd75ec5e2854c6f576b00d405560633f5fa Binary files /dev/null and b/src/pdf2u/__pycache__/pdfinterp.cpython-312.pyc differ diff --git a/src/pdf2u/__pycache__/progress_monitor.cpython-311.pyc b/src/pdf2u/__pycache__/progress_monitor.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..24fa582c9ef8993d5f4ec19072521ceb72b41709 Binary files /dev/null and b/src/pdf2u/__pycache__/progress_monitor.cpython-311.pyc differ diff --git a/src/pdf2u/__pycache__/progress_monitor.cpython-312.pyc b/src/pdf2u/__pycache__/progress_monitor.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a544d8c44a837d15760102fbede6a2f78a848f02 Binary files /dev/null and b/src/pdf2u/__pycache__/progress_monitor.cpython-312.pyc differ diff --git a/src/pdf2u/__pycache__/translation_config.cpython-311.pyc b/src/pdf2u/__pycache__/translation_config.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d553803463ba793034a1b8449bd20533895f9b23 Binary files /dev/null and b/src/pdf2u/__pycache__/translation_config.cpython-311.pyc differ diff --git a/src/pdf2u/__pycache__/translation_config.cpython-312.pyc b/src/pdf2u/__pycache__/translation_config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b03fbc4cb67934c07f768c1e84a211db3c9c9af2 Binary files /dev/null and b/src/pdf2u/__pycache__/translation_config.cpython-312.pyc differ diff --git a/src/pdf2u/asynchronize/__init__.py b/src/pdf2u/asynchronize/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..cf23d13356e0cc32750135a9a2993993a4a901f5 --- /dev/null +++ b/src/pdf2u/asynchronize/__init__.py @@ -0,0 +1,51 @@ +import asyncio +import time + + +class Args: + def __init__(self, args, kwargs): + self.args = args + self.kwargs = kwargs + + +class AsyncCallback: + def __init__(self): + self.queue = asyncio.Queue() + self.finished = False + self.loop = asyncio.get_event_loop() + + def step_callback(self, *args, **kwargs): + # Whenever a step is called, add to the queue but don't set finished to True, so __anext__ will continue + args = Args(args, kwargs) + + # We have to use the threadsafe call so that it wakes up the event loop, in case it's sleeping: + # https://stackoverflow.com/a/49912853/2148718 + self.loop.call_soon_threadsafe(self.queue.put_nowait, args) + + # Add a small delay to release the GIL, ensuring the event loop has time to process messages + time.sleep(0.01) + + def finished_callback(self, *args, **kwargs): + # Whenever a finished is called, add to the queue as with step, but also set finished to True, so __anext__ + # will terminate after processing the remaining items + if self.finished: + return + self.step_callback(*args, **kwargs) + self.finished = True + + def __await__(self): + # Since this implements __anext__, this can return itself + return self.queue.get().__await__() + + def __aiter__(self): + # Since this implements __anext__, this can return itself + return self + + async def __anext__(self): + # Keep waiting for the queue if a) we haven't finished, or b) if the queue is still full. This lets us finish + # processing the remaining items even after we've finished + if self.finished and self.queue.empty(): + raise StopAsyncIteration + + result = await self.queue.get() + return result diff --git a/src/pdf2u/asynchronize/__pycache__/__init__.cpython-311.pyc b/src/pdf2u/asynchronize/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c1a8ec73f0a3161e95d23ef0cdb565a405604596 Binary files /dev/null and b/src/pdf2u/asynchronize/__pycache__/__init__.cpython-311.pyc differ diff --git a/src/pdf2u/asynchronize/__pycache__/__init__.cpython-312.pyc b/src/pdf2u/asynchronize/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..91b60165348bc68289ea289dac6a944f2b61f6ed Binary files /dev/null and b/src/pdf2u/asynchronize/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/pdf2u/const.py b/src/pdf2u/const.py new file mode 100644 index 0000000000000000000000000000000000000000..534d3059161e92badc377e0119d9c7982372b4d7 --- /dev/null +++ b/src/pdf2u/const.py @@ -0,0 +1,14 @@ +from enum import StrEnum +from pathlib import Path + +CACHE_FOLDER = Path.home() / ".cache" / "pdf2u" + + +def get_cache_file_path(filename: str) -> Path: + return CACHE_FOLDER / filename + + +class TranslationService(StrEnum): + OPENAI: str = "openai" + GOOGLE: str = "google" + BING: str = "bing" diff --git a/src/pdf2u/converter.py b/src/pdf2u/converter.py new file mode 100644 index 0000000000000000000000000000000000000000..f02da62468805e805d237c999cef4713c4c63dbf --- /dev/null +++ b/src/pdf2u/converter.py @@ -0,0 +1,493 @@ +import base64 +import logging +import re +import unicodedata + +import numpy as np +from pdfminer.converter import PDFConverter +from pdfminer.layout import LTChar, LTComponent, LTFigure, LTLine, LTPage, LTText +from pdfminer.pdfcolor import PDFColorSpace +from pdfminer.pdffont import PDFCIDFont, PDFFont, PDFUnicodeNotDefined +from pdfminer.pdfinterp import PDFGraphicState, PDFResourceManager +from pdfminer.utils import Matrix, apply_matrix_pt, bbox2str, matrix2str, mult_matrix +from pymupdf import Font + +from pdf2u.document_il.frontend.il_creater import ILCreater + +log = logging.getLogger(__name__) + + +class PDFConverterEx(PDFConverter): + def __init__( + self, rsrcmgr: PDFResourceManager, il_creater: ILCreater | None = None + ) -> None: + PDFConverter.__init__(self, rsrcmgr, None, "utf-8", 1, None) + self.il_creater = il_creater + + def begin_page(self, page, ctm) -> None: + # 重载替换 cropbox + (x0, y0, x1, y1) = page.cropbox + (x0, y0) = apply_matrix_pt(ctm, (x0, y0)) + (x1, y1) = apply_matrix_pt(ctm, (x1, y1)) + mediabox = (0, 0, abs(x0 - x1), abs(y0 - y1)) + self.il_creater.on_page_media_box( + mediabox[0], mediabox[1], mediabox[2], mediabox[3] + ) + self.il_creater.on_page_number(page.pageno) + self.cur_item = LTPage(page.pageno, mediabox) + + def end_page(self, _page) -> None: + # 重载返回指令流 + return self.receive_layout(self.cur_item) + + def begin_figure(self, name, bbox, matrix) -> None: + # 重载设置 pageid + self._stack.append(self.cur_item) + self.cur_item = LTFigure(name, bbox, mult_matrix(matrix, self.ctm)) + self.cur_item.pageid = self._stack[-1].pageid + + def end_figure(self, _: str) -> None: + # 重载返回指令流 + fig = self.cur_item + if not isinstance(self.cur_item, LTFigure): + raise ValueError(f"Unexpected item type: {type(self.cur_item)}") + self.cur_item = self._stack.pop() + self.cur_item.add(fig) + return self.receive_layout(fig) + + def render_char( + self, + matrix, + font, + fontsize: float, + scaling: float, + rise: float, + cid: int, + ncs, + graphicstate: PDFGraphicState, + ) -> float: + # 重载设置 cid 和 font + try: + text = font.to_unichr(cid) + if not isinstance(text, str): + raise TypeError(f"Expected string, got {type(text)}") + except PDFUnicodeNotDefined: + text = self.handle_undefined_char(font, cid) + textwidth = font.char_width(cid) + textdisp = font.char_disp(cid) + + font_name = font.fontname + if isinstance(font_name, bytes): + try: + font_name = font_name.decode("utf-8") + except UnicodeDecodeError: + font_name = "BASE64:" + base64.b64encode(font_name).decode("utf-8") + font_id = self.il_creater.current_page_font_name_id_map[font_name] + + item = AWLTChar( + matrix, + font, + fontsize, + scaling, + rise, + text, + textwidth, + textdisp, + ncs, + graphicstate, + self.il_creater.xobj_id, + font_id, + ) + self.cur_item.add(item) + item.cid = cid # hack 插入原字符编码 + item.font = font # hack 插入原字符字体 + return item.adv + + +class AWLTChar(LTChar): + """Actual letter in the text as a Unicode string.""" + + def __init__( + self, + matrix: Matrix, + font: PDFFont, + fontsize: float, + scaling: float, + rise: float, + text: str, + textwidth: float, + textdisp: float | tuple[float | None, float], + ncs: PDFColorSpace, + graphicstate: PDFGraphicState, + xobj_id: int, + font_id: str, + ) -> None: + LTText.__init__(self) + self._text = text + self.matrix = matrix + self.fontname = font.fontname + self.ncs = ncs + self.graphicstate = graphicstate + self.xobj_id = xobj_id + self.adv = textwidth * fontsize * scaling + self.aw_font_id = font_id + # compute the boundary rectangle. + if font.is_vertical(): + # vertical + assert isinstance(textdisp, tuple) + (vx, vy) = textdisp + if vx is None: + vx = fontsize * 0.5 + else: + vx = vx * fontsize * 0.001 + vy = (1000 - vy) * fontsize * 0.001 + bbox_lower_left = (-vx, vy + rise + self.adv) + bbox_upper_right = (-vx + fontsize, vy + rise) + else: + # horizontal + descent = font.get_descent() * fontsize + bbox_lower_left = (0, descent + rise) + bbox_upper_right = (self.adv, descent + rise + fontsize) + (a, b, c, d, e, f) = self.matrix + self.upright = a * d * scaling > 0 and b * c <= 0 + (x0, y0) = apply_matrix_pt(self.matrix, bbox_lower_left) + (x1, y1) = apply_matrix_pt(self.matrix, bbox_upper_right) + if x1 < x0: + (x0, x1) = (x1, x0) + if y1 < y0: + (y0, y1) = (y1, y0) + LTComponent.__init__(self, (x0, y0, x1, y1)) + if font.is_vertical() or matrix[0] == 0: + self.size = self.width + else: + self.size = self.height + return + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} {bbox2str(self.bbox)} matrix={matrix2str(self.matrix)} font={self.fontname!r} adv={self.adv} text={self.get_text()!r}>" + + def get_text(self) -> str: + return self._text + + +class Paragraph: + def __init__(self, y, x, x0, x1, size, brk): + self.y: float = y # 初始纵坐标 + self.x: float = x # 初始横坐标 + self.x0: float = x0 # 左边界 + self.x1: float = x1 # 右边界 + self.size: float = size # 字体大小 + self.brk: bool = brk # 换行标记 + + +# fmt: off +class TranslateConverter(PDFConverterEx): + def __init__( + self, + rsrcmgr, + vfont: str | None = None, + vchar: str | None = None, + thread: int = 0, + layout: dict | None = None, + lang_in: str = "", # 保留参数但添加未使用标记 + _lang_out: str = "", # 改为未使用参数 + _service: str = "", # 改为未使用参数 + resfont: str = "", + noto: Font | None = None, + envs: dict | None = None, + _prompt: list | None = None, # 改为未使用参数 + il_creater: ILCreater | None = None, + ): + layout = layout or {} + super().__init__(rsrcmgr, il_creater) + self.vfont = vfont + self.vchar = vchar + self.thread = thread + self.layout = layout + self.resfont = resfont + self.noto = noto + + def receive_layout(self, ltpage: LTPage): + # 段落 + sstk: list[str] = [] # 段落文字栈 + pstk: list[Paragraph] = [] # 段落属性栈 + vbkt: int = 0 # 段落公式括号计数 + # 公式组 + vstk: list[LTChar] = [] # 公式符号组 + vlstk: list[LTLine] = [] # 公式线条组 + vfix: float = 0 # 公式纵向偏移 + # 公式组栈 + var: list[list[LTChar]] = [] # 公式符号组栈 + varl: list[list[LTLine]] = [] # 公式线条组栈 + varf: list[float] = [] # 公式纵向偏移栈 + vlen: list[float] = [] # 公式宽度栈 + # 全局 + lstk: list[LTLine] = [] # 全局线条栈 + xt: LTChar = None # 上一个字符 + xt_cls: int = -1 # 上一个字符所属段落,保证无论第一个字符属于哪个类别都可以触发新段落 + vmax: float = ltpage.width / 4 # 行内公式最大宽度 + ops: str = "" # 渲染结果 + + def vflag(font: str, char: str): # 匹配公式(和角标)字体 + if isinstance(font, bytes): # 不一定能 decode,直接转 str + font = str(font) + font = font.split("+")[-1] # 字体名截断 + if re.match(r"\(cid:", char): + return True + # 基于字体名规则的判定 + if self.vfont: + if re.match(self.vfont, font): + return True + else: + if re.match( # latex 字体 + r"(CM[^R]|(MS|XY|MT|BL|RM|EU|LA|RS)[A-Z]|LINE|LCIRCLE|TeX-|rsfs|txsy|wasy|stmary|.*Mono|.*Code|.*Ital|.*Sym|.*Math)", + font, + ): + return True + # 基于字符集规则的判定 + if self.vchar: + if re.match(self.vchar, char): + return True + else: + if ( + char + and char != " " # 非空格 + and ( + unicodedata.category(char[0]) + in ["Lm", "Mn", "Sk", "Sm", "Zl", "Zp", "Zs"] # 文字修饰符、数学符号、分隔符号 + or ord(char[0]) in range(0x370, 0x400) # 希腊字母 + ) + ): + return True + return False + + ############################################################ + # A. 原文档解析 + for child in ltpage: + if isinstance(child, LTChar): + self.il_creater.on_lt_char(child) + continue + cur_v = False + layout = self.layout[ltpage.pageid] + # ltpage.height 可能是 fig 里面的高度,这里统一用 layout.shape + h, w = layout.shape + # 读取当前字符在 layout 中的类别 + cx, cy = np.clip(int(child.x0), 0, w - 1), np.clip(int(child.y0), 0, h - 1) + cls = layout[cy, cx] + # 锚定文档中 bullet 的位置 + if child.get_text() == "•": + cls = 0 + # 判定当前字符是否属于公式 + if ( # 判定当前字符是否属于公式 + cls == 0 # 1. 类别为保留区域 + or (cls == xt_cls and len(sstk[-1].strip()) > 1 and child.size < pstk[-1].size * 0.79) # 2. 角标字体,有 0.76 的角标和 0.799 的大写,这里用 0.79 取中,同时考虑首字母放大的情况 + or vflag(child.fontname, child.get_text()) # 3. 公式字体 + or (child.matrix[0] == 0 and child.matrix[3] == 0) # 4. 垂直字体 + ): + cur_v = True + # 判定括号组是否属于公式 + if not cur_v: + if vstk and child.get_text() == "(": + cur_v = True + vbkt += 1 + if vbkt and child.get_text() == ")": + cur_v = True + vbkt -= 1 + if ( # 判定当前公式是否结束 + not cur_v # 1. 当前字符不属于公式 + or cls != xt_cls # 2. 当前字符与前一个字符不属于同一段落 + # or (abs(child.x0 - xt.x0) > vmax and cls != 0) # 3. 段落内换行,可能是一长串斜体的段落,也可能是段内分式换行,这里设个阈值进行区分 + # 禁止纯公式(代码)段落换行,直到文字开始再重开文字段落,保证只存在两种情况 + # A. 纯公式(代码)段落(锚定绝对位置)sstk[-1]=="" -> sstk[-1]=="{v*}" + # B. 文字开头段落(排版相对位置)sstk[-1]!="" + or (sstk[-1] != "" and abs(child.x0 - xt.x0) > vmax) # 因为 cls==xt_cls==0 一定有 sstk[-1]=="",所以这里不需要再判定 cls!=0 + ): + if vstk: + if ( # 根据公式右侧的文字修正公式的纵向偏移 + not cur_v # 1. 当前字符不属于公式 + and cls == xt_cls # 2. 当前字符与前一个字符属于同一段落 + and child.x0 > max([vch.x0 for vch in vstk]) # 3. 当前字符在公式右侧 + ): + vfix = vstk[0].y0 - child.y0 + if sstk[-1] == "": + xt_cls = -1 # 禁止纯公式段落(sstk[-1]=="{v*}")的后续连接,但是要考虑新字符和后续字符的连接,所以这里修改的是上个字符的类别 + sstk[-1] += f"{{v{len(var)}}}" + var.append(vstk) + varl.append(vlstk) + varf.append(vfix) + vstk = [] + vlstk = [] + vfix = 0 + # 当前字符不属于公式或当前字符是公式的第一个字符 + if not vstk: + if cls == xt_cls: # 当前字符与前一个字符属于同一段落 + if child.x0 > xt.x1 + 1: # 添加行内空格 + sstk[-1] += " " + elif child.x1 < xt.x0: # 添加换行空格并标记原文段落存在换行 + sstk[-1] += " " + pstk[-1].brk = True + else: # 根据当前字符构建一个新的段落 + sstk.append("") + pstk.append(Paragraph(child.y0, child.x0, child.x0, child.x0, child.size, False)) + if not cur_v: # 文字入栈 + if ( # 根据当前字符修正段落属性 + child.size > pstk[-1].size / 0.79 # 1. 当前字符显著比段落字体大 + or len(sstk[-1].strip()) == 1 # 2. 当前字符为段落第二个文字(考虑首字母放大的情况) + ) and child.get_text() != " ": # 3. 当前字符不是空格 + pstk[-1].y -= child.size - pstk[-1].size # 修正段落初始纵坐标,假设两个不同大小字符的上边界对齐 + pstk[-1].size = child.size + sstk[-1] += child.get_text() + else: # 公式入栈 + if ( # 根据公式左侧的文字修正公式的纵向偏移 + not vstk # 1. 当前字符是公式的第一个字符 + and cls == xt_cls # 2. 当前字符与前一个字符属于同一段落 + and child.x0 > xt.x0 # 3. 前一个字符在公式左侧 + ): + vfix = child.y0 - xt.y0 + vstk.append(child) + # 更新段落边界,因为段落内换行之后可能是公式开头,所以要在外边处理 + pstk[-1].x0 = min(pstk[-1].x0, child.x0) + pstk[-1].x1 = max(pstk[-1].x1, child.x1) + # 更新上一个字符 + xt = child + xt_cls = cls + elif isinstance(child, LTFigure): + # 图表 + self.il_creater.on_pdf_figure(child) + pass + elif isinstance(child, LTLine): # 线条 + continue + layout = self.layout[ltpage.pageid] + # ltpage.height 可能是 fig 里面的高度,这里统一用 layout.shape + h, w = layout.shape + # 读取当前线条在 layout 中的类别 + cx, cy = np.clip(int(child.x0), 0, w - 1), np.clip(int(child.y0), 0, h - 1) + cls = layout[cy, cx] + if vstk and cls == xt_cls: # 公式线条 + vlstk.append(child) + else: # 全局线条 + lstk.append(child) + else: + pass + return + # 处理结尾 + if vstk: # 公式出栈 + sstk[-1] += f"{{v{len(var)}}}" + var.append(vstk) + varl.append(vlstk) + varf.append(vfix) + log.debug("\n==========[VSTACK]==========\n") + for var_id, v in enumerate(var): # 计算公式宽度 + l = max([vch.x1 for vch in v]) - v[0].x0 + log.debug(f'< {l:.1f} {v[0].x0:.1f} {v[0].y0:.1f} {v[0].cid} {v[0].fontname} {len(varl[var_id])} > v{var_id} = {"".join([ch.get_text() for ch in v])}') + vlen.append(l) + + ############################################################ + # B. 段落翻译 + log.debug("\n==========[SSTACK]==========\n") + + news = sstk.copy() + + ############################################################ + # C. 新文档排版 + def raw_string(fcur: str, cstk: str): # 编码字符串 + if fcur == 'noto': + return "".join([f"{self.noto.has_glyph(ord(c)):04x}" for c in cstk]) + elif isinstance(self.fontmap[fcur], PDFCIDFont): # 判断编码长度 + return "".join([f"{ord(c):04x}" for c in cstk]) + else: + return "".join([f"{ord(c):02x}" for c in cstk]) + + _x, _y = 0, 0 + for para_id, new in enumerate(news): + x: float = pstk[para_id].x # 段落初始横坐标 + y: float = pstk[para_id].y # 段落初始纵坐标 + x0: float = pstk[para_id].x0 # 段落左边界 + x1: float = pstk[para_id].x1 # 段落右边界 + size: float = pstk[para_id].size # 段落字体大小 + brk: bool = pstk[para_id].brk # 段落换行标记 + cstk: str = "" # 当前文字栈 + fcur: str = None # 当前字体 ID + tx = x + fcur_ = fcur + ptr = 0 + log.debug(f"< {y} {x} {x0} {x1} {size} {brk} > {sstk[para_id]} | {new}") + while ptr < len(new): + vy_regex = re.match( + r"\{\s*v([\d\s]+)\}", new[ptr:], re.IGNORECASE, + ) # 匹配 {vn} 公式标记 + mod = 0 # 文字修饰符 + if vy_regex: # 加载公式 + ptr += len(vy_regex.group(0)) + try: + vid = int(vy_regex.group(1).replace(" ", "")) + adv = vlen[vid] + except Exception as e: + log.debug("Skipping formula placeholder due to: %s", e) + continue # 翻译器可能会自动补个越界的公式标记 + if var[vid][-1].get_text() and unicodedata.category(var[vid][-1].get_text()[0]) in ["Lm", "Mn", "Sk"]: # 文字修饰符 + mod = var[vid][-1].width + else: # 加载文字 + ch = new[ptr] + fcur_ = None + try: + if fcur_ is None and self.fontmap["tiro"].to_unichr(ord(ch)) == ch: + fcur_ = "tiro" # 默认拉丁字体 + except Exception: + pass + if fcur_ is None: + fcur_ = self.resfont # 默认非拉丁字体 + if fcur_ == 'noto': + adv = self.noto.char_lengths(ch, size)[0] + else: + adv = self.fontmap[fcur_].char_width(ord(ch)) * size + ptr += 1 + if ( # 输出文字缓冲区 + fcur_ != fcur # 1. 字体更新 + or vy_regex # 2. 插入公式 + or x + adv > x1 + 0.1 * size # 3. 到达右边界(可能一整行都被符号化,这里需要考虑浮点误差) + ): + if cstk: + ops += f"/{fcur} {size:f} Tf 1 0 0 1 {tx:f} {y:f} Tm [<{raw_string(fcur, cstk)}>] TJ " + cstk = "" + if brk and x + adv > x1 + 0.1 * size: # 到达右边界且原文段落存在换行 + x = x0 + lang_space = {"zh-cn": 1.4, "zh-tw": 1.4, "zh-hans": 1.4, "zh-hant": 1.4, "zh": 1.4, "ja": 1.1, "ko": 1.2, "en": 1.2, "ar": 1.0, "ru": 0.8, "uk": 0.8, "ta": 0.8} + # y -= size * lang_space.get(self.translator.lang_out.lower(), 1.1) # 小语种大多适配 1.1 + y -= size * 1.4 + if vy_regex: # 插入公式 + fix = 0 + if fcur is not None: # 段落内公式修正纵向偏移 + fix = varf[vid] + for vch in var[vid]: # 排版公式字符 + vc = chr(vch.cid) + ops += f"/{self.fontid[vch.font]} {vch.size:f} Tf 1 0 0 1 {x + vch.x0 - var[vid][0].x0:f} {fix + y + vch.y0 - var[vid][0].y0:f} Tm <{raw_string(self.fontid[vch.font], vc)}> TJ " + if log.isEnabledFor(logging.DEBUG): + lstk.append(LTLine(0.1, (_x, _y), (x + vch.x0 - var[vid][0].x0, fix + y + vch.y0 - var[vid][0].y0))) + _x, _y = x + vch.x0 - var[vid][0].x0, fix + y + vch.y0 - var[vid][0].y0 + for l in varl[vid]: # 排版公式线条 + if l.linewidth < 5: # hack 有的文档会用粗线条当图片背景 + ops += f"ET q 1 0 0 1 {l.pts[0][0] + x - var[vid][0].x0:f} {l.pts[0][1] + fix + y - var[vid][0].y0:f} cm [] 0 d 0 J {l.linewidth:f} w 0 0 m {l.pts[1][0] - l.pts[0][0]:f} {l.pts[1][1] - l.pts[0][1]:f} l S Q BT " + else: # 插入文字缓冲区 + if not cstk: # 单行开头 + tx = x + if x == x0 and ch == " ": # 消除段落换行空格 + adv = 0 + else: + cstk += ch + else: + cstk += ch + adv -= mod # 文字修饰符 + fcur = fcur_ + x += adv + if log.isEnabledFor(logging.DEBUG): + lstk.append(LTLine(0.1, (_x, _y), (x, y))) + _x, _y = x, y + # 处理结尾 + if cstk: + ops += f"/{fcur} {size:f} Tf 1 0 0 1 {tx:f} {y:f} Tm <{raw_string(fcur, cstk)}> TJ " + for l in lstk: # 排版全局线条 + if l.linewidth < 5: # hack 有的文档会用粗线条当图片背景 + ops += f"ET q 1 0 0 1 {l.pts[0][0]:f} {l.pts[0][1]:f} cm [] 0 d 0 J {l.linewidth:f} w 0 0 m {l.pts[1][0] - l.pts[0][0]:f} {l.pts[1][1] - l.pts[0][1]:f} l S Q BT " + ops = f"BT {ops}ET " + return ops diff --git a/src/pdf2u/document_il/__init__.py b/src/pdf2u/document_il/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..70915106d4081ef310c6e0ba5e195be2d826326a --- /dev/null +++ b/src/pdf2u/document_il/__init__.py @@ -0,0 +1,45 @@ +from pdf2u.document_il.il_version_1 import ( + BaseOperations, + Box, + Cropbox, + Document, + GraphicState, + Mediabox, + Page, + PageLayout, + PdfCharacter, + PdfFigure, + PdfFont, + PdfFormula, + PdfLine, + PdfParagraph, + PdfParagraphComposition, + PdfRectangle, + PdfSameStyleCharacters, + PdfSameStyleUnicodeCharacters, + PdfStyle, + PdfXobject, +) + +__all__ = [ + "BaseOperations", + "Box", + "Cropbox", + "Document", + "GraphicState", + "Mediabox", + "Page", + "PageLayout", + "PdfCharacter", + "PdfFigure", + "PdfFont", + "PdfFormula", + "PdfLine", + "PdfParagraph", + "PdfParagraphComposition", + "PdfRectangle", + "PdfSameStyleCharacters", + "PdfSameStyleUnicodeCharacters", + "PdfStyle", + "PdfXobject", +] diff --git a/src/pdf2u/document_il/__pycache__/__init__.cpython-311.pyc b/src/pdf2u/document_il/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5ec9e35bf8d9960ba09afe14257cb61a450e5c6a Binary files /dev/null and b/src/pdf2u/document_il/__pycache__/__init__.cpython-311.pyc differ diff --git a/src/pdf2u/document_il/__pycache__/__init__.cpython-312.pyc b/src/pdf2u/document_il/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..82f09d89e9bbce5448b32c6b4a42a858ca703fab Binary files /dev/null and b/src/pdf2u/document_il/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/pdf2u/document_il/__pycache__/il_version_1.cpython-311.pyc b/src/pdf2u/document_il/__pycache__/il_version_1.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bdd456558782eb2f52d1ec228d589462c186405b Binary files /dev/null and b/src/pdf2u/document_il/__pycache__/il_version_1.cpython-311.pyc differ diff --git a/src/pdf2u/document_il/__pycache__/il_version_1.cpython-312.pyc b/src/pdf2u/document_il/__pycache__/il_version_1.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..20e67070c5beaa917e860c71dd2f3fb636f66aa8 Binary files /dev/null and b/src/pdf2u/document_il/__pycache__/il_version_1.cpython-312.pyc differ diff --git a/src/pdf2u/document_il/__pycache__/xml_converter.cpython-311.pyc b/src/pdf2u/document_il/__pycache__/xml_converter.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..68019df2fe33717cf0dc4e922ffd61f8ff67efed Binary files /dev/null and b/src/pdf2u/document_il/__pycache__/xml_converter.cpython-311.pyc differ diff --git a/src/pdf2u/document_il/__pycache__/xml_converter.cpython-312.pyc b/src/pdf2u/document_il/__pycache__/xml_converter.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b7bd9dd185e923de4b30e8e89aa18cb2acc85eab Binary files /dev/null and b/src/pdf2u/document_il/__pycache__/xml_converter.cpython-312.pyc differ diff --git a/src/pdf2u/document_il/backend/__init__.py b/src/pdf2u/document_il/backend/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/pdf2u/document_il/backend/__pycache__/__init__.cpython-311.pyc b/src/pdf2u/document_il/backend/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..17895ec5e2a51196fe67ee6e60b53f4ce812ffed Binary files /dev/null and b/src/pdf2u/document_il/backend/__pycache__/__init__.cpython-311.pyc differ diff --git a/src/pdf2u/document_il/backend/__pycache__/__init__.cpython-312.pyc b/src/pdf2u/document_il/backend/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fb2ad98de32f3a8870593bb5adc3d5cd4163e431 Binary files /dev/null and b/src/pdf2u/document_il/backend/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/pdf2u/document_il/backend/__pycache__/pdf_creater.cpython-311.pyc b/src/pdf2u/document_il/backend/__pycache__/pdf_creater.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..65ea5a0e8dd2d208be186c40ace7636b9c29597d Binary files /dev/null and b/src/pdf2u/document_il/backend/__pycache__/pdf_creater.cpython-311.pyc differ diff --git a/src/pdf2u/document_il/backend/__pycache__/pdf_creater.cpython-312.pyc b/src/pdf2u/document_il/backend/__pycache__/pdf_creater.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3303fa9591c4a56d871f19ddca93a6ef9760bd7e Binary files /dev/null and b/src/pdf2u/document_il/backend/__pycache__/pdf_creater.cpython-312.pyc differ diff --git a/src/pdf2u/document_il/backend/pdf_creater.py b/src/pdf2u/document_il/backend/pdf_creater.py new file mode 100644 index 0000000000000000000000000000000000000000..5066f90ccea6e2c4257282c25d5aaa18377c7b7c --- /dev/null +++ b/src/pdf2u/document_il/backend/pdf_creater.py @@ -0,0 +1,405 @@ +import logging +import re +from pathlib import Path + +import pymupdf +from bitstring import BitStream + +from pdf2u.document_il import il_version_1 +from pdf2u.document_il.utils.fontmap import FontMapper +from pdf2u.translation_config import TranslateResult, TranslationConfig + +logger = logging.getLogger(__name__) + +SUBSET_FONT_STAGE_NAME = "Subset font" +SAVE_PDF_STAGE_NAME = "Save PDF" + + +class PDFCreater: + stage_name = "Generate drawing instructions" + + def __init__( + self, + original_pdf_path: str, + document: il_version_1.Document, + translation_config: TranslationConfig, + ): + self.original_pdf_path = original_pdf_path + self.docs = document + self.font_path = translation_config.font + self.font_mapper = FontMapper(translation_config) + self.translation_config = translation_config + + def render_graphic_state( + self, draw_op: BitStream, graphic_state: il_version_1.GraphicState + ): + if graphic_state is None: + return + # if graphic_state.stroking_color_space_name: + # draw_op.append( + # f"/{graphic_state.stroking_color_space_name} CS \n".encode() + # ) + # if graphic_state.non_stroking_color_space_name: + # draw_op.append( + # f"/{graphic_state.non_stroking_color_space_name}" + # f" cs \n".encode() + # ) + # if graphic_state.ncolor is not None: + # if len(graphic_state.ncolor) == 1: + # draw_op.append(f"{graphic_state.ncolor[0]} g \n".encode()) + # elif len(graphic_state.ncolor) == 3: + # draw_op.append( + # f"{' '.join((str(x) for x in graphic_state.ncolor))} sc \n".encode() + # ) + # if graphic_state.scolor is not None: + # if len(graphic_state.scolor) == 1: + # draw_op.append(f"{graphic_state.scolor[0]} G \n".encode()) + # elif len(graphic_state.scolor) == 3: + # draw_op.append( + # f"{' '.join((str(x) for x in graphic_state.scolor))} SC \n".encode() + # ) + + if graphic_state.passthrough_per_char_instruction: + draw_op.append( + f"{graphic_state.passthrough_per_char_instruction} \n".encode() + ) + + def render_paragraph_to_char( + self, paragraph: il_version_1.PdfParagraph + ) -> list[il_version_1.PdfCharacter]: + chars = [] + for composition in paragraph.pdf_paragraph_composition: + if not isinstance(composition.pdf_character, il_version_1.PdfCharacter): + logger.error( + f"Unknown composition type. " + f"This type only appears in the IL " + f"after the translation is completed." + f"During pdf rendering, this type is not supported." + f"Composition: {composition}. " + f"Paragraph: {paragraph}. " + ) + continue + chars.append(composition.pdf_character) + if not chars and paragraph.unicode: + logger.error( + f"Unable to export paragraphs that have " + f"not yet been formatted: {paragraph}" + ) + return chars + return chars + + def get_available_font_list(self, pdf, page): + page_xref_id = pdf[page.page_number].xref + return self.get_xobj_available_fonts(page_xref_id, pdf) + + def get_xobj_available_fonts(self, page_xref_id, pdf): + resources_type, r_id = pdf.xref_get_key(page_xref_id, "Resources") + if resources_type == "xref": + resource_xref_id = re.search("(\\d+) 0 R", r_id).group(1) + r_id = pdf.xref_object(int(resource_xref_id)) + resources_type = "dict" + if resources_type == "dict": + xref_id = re.search("/Font (\\d+) 0 R", r_id) + if xref_id is not None: + xref_id = xref_id.group(1) + font_dict = pdf.xref_object(int(xref_id)) + else: + search = re.search("/Font *<<(.+?)>>", r_id.replace("\n", " ")) + if search is None: + # Have resources but no fonts + return set() + font_dict = search.group(1) + else: + r_id = int(r_id.split(" ")[0]) + _, font_dict = pdf.xref_get_key(r_id, "Font") + fonts = re.findall("/([^ ]+?) ", font_dict) + return set(fonts) + + def _debug_render_rectangle( + self, draw_op: BitStream, rectangle: il_version_1.PdfRectangle + ): + """Draw a debug rectangle in PDF for visualization purposes. + + Args: + draw_op: BitStream to append PDF drawing operations + rectangle: Rectangle object containing position information + """ + x1 = rectangle.box.x + y1 = rectangle.box.y + x2 = rectangle.box.x2 + y2 = rectangle.box.y2 + # Save graphics state + draw_op.append(b"q ") + + # Set green color for debug visibility + draw_op.append( + rectangle.graphic_state.passthrough_per_char_instruction.encode() + ) # Green stroke + draw_op.append(b" 1 w ") # Line width + + # Draw four lines manually + # Bottom line + draw_op.append(f"{x1} {y1} m {x2} {y1} l S ".encode()) + # Right line + draw_op.append(f"{x2} {y1} m {x2} {y2} l S ".encode()) + # Top line + draw_op.append(f"{x2} {y2} m {x1} {y2} l S ".encode()) + # Left line + draw_op.append(f"{x1} {y2} m {x1} {y1} l S ".encode()) + + # Restore graphics state + draw_op.append(b"Q\n") + + def write_debug_info( + self, pdf: pymupdf.Document, translation_config: TranslationConfig + ): + self.font_mapper.add_font(pdf, self.docs) + + for page in self.docs.page: + _, r_id = pdf.xref_get_key(pdf[page.page_number].xref, "Contents") + resource_xref_id = re.search("(\\d+) 0 R", r_id).group(1) + base_op = pdf.xref_stream(int(resource_xref_id)) + translation_config.raise_if_cancelled() + xobj_available_fonts = {} + xobj_draw_ops = {} + xobj_encoding_length_map = {} + available_font_list = self.get_available_font_list(pdf, page) + + page_encoding_length_map = { + f.font_id: f.encoding_length for f in page.pdf_font + } + page_op = BitStream() + # q {ops_base}Q 1 0 0 1 {x0} {y0} cm {ops_new} + page_op.append(b"q ") + if base_op is not None: + page_op.append(base_op) + page_op.append(b" Q ") + page_op.append( + f"q Q 1 0 0 1 {page.cropbox.box.x} {page.cropbox.box.y} cm \n".encode() + ) + # 收集所有字符 + chars = [] + # 首先添加页面级别的字符 + if page.pdf_character: + chars.extend(page.pdf_character) + # 然后添加段落中的字符 + for paragraph in page.pdf_paragraph: + chars.extend(self.render_paragraph_to_char(paragraph)) + + # 渲染所有字符 + for char in chars: + if not getattr(char, "debug_info", False): + continue + if char.char_unicode == "\n": + continue + if char.pdf_character_id is None: + # dummy char + continue + char_size = char.pdf_style.font_size + font_id = char.pdf_style.font_id + + if font_id not in available_font_list: + continue + draw_op = page_op + encoding_length_map = page_encoding_length_map + + draw_op.append(b"q ") + self.render_graphic_state(draw_op, char.pdf_style.graphic_state) + if char.vertical: + draw_op.append( + f"BT /{font_id} {char_size:f} Tf 0 1 -1 0 {char.box.x2:f} {char.box.y:f} Tm ".encode() + ) + else: + draw_op.append( + f"BT /{font_id} {char_size:f} Tf 1 0 0 1 {char.box.x:f} {char.box.y:f} Tm ".encode() + ) + + encoding_length = encoding_length_map[font_id] + # pdf32000-2008 page14: + # As hexadecimal data enclosed in angle brackets < > + # see 7.3.4.3, "Hexadecimal Strings." + draw_op.append( + f"<{char.pdf_character_id:0{encoding_length * 2}x}>".upper().encode() + ) + + draw_op.append(b" Tj ET Q \n") + for rect in page.pdf_rectangle: + if not rect.debug_info: + continue + self._debug_render_rectangle(page_op, rect) + draw_op = page_op + # Since this is a draw instruction container, + # no additional information is needed + pdf.update_stream(int(resource_xref_id), draw_op.tobytes()) + translation_config.raise_if_cancelled() + pdf.subset_fonts(fallback=False) + + def write(self, translation_config: TranslationConfig) -> TranslateResult: + basename = Path(translation_config.input_file).stem + debug_suffix = ".debug" if translation_config.debug else "" + mono_out_path = translation_config.get_output_file_path( + f"{basename}{debug_suffix}.{translation_config.lang_out}.mono.pdf" + ) + pdf = pymupdf.open(self.original_pdf_path) + self.font_mapper.add_font(pdf, self.docs) + with self.translation_config.progress_monitor.stage_start( + self.stage_name, len(self.docs.page) + ) as pbar: + for page in self.docs.page: + translation_config.raise_if_cancelled() + xobj_available_fonts = {} + xobj_draw_ops = {} + xobj_encoding_length_map = {} + available_font_list = self.get_available_font_list(pdf, page) + + for xobj in page.pdf_xobject: + xobj_available_fonts[xobj.xobj_id] = available_font_list.copy() + try: + xobj_available_fonts[xobj.xobj_id].update( + self.get_xobj_available_fonts(xobj.xref_id, pdf) + ) + except Exception: + pass + xobj_encoding_length_map[xobj.xobj_id] = { + f.font_id: f.encoding_length for f in xobj.pdf_font + } + xobj_op = BitStream() + xobj_op.append(xobj.base_operations.value.encode()) + xobj_draw_ops[xobj.xobj_id] = xobj_op + page_encoding_length_map = { + f.font_id: f.encoding_length for f in page.pdf_font + } + page_op = BitStream() + # q {ops_base}Q 1 0 0 1 {x0} {y0} cm {ops_new} + page_op.append(b"q ") + page_op.append(page.base_operations.value.encode()) + page_op.append(b" Q ") + page_op.append( + f"q Q 1 0 0 1 {page.cropbox.box.x} {page.cropbox.box.y} cm \n".encode() + ) + # 收集所有字符 + chars = [] + # 首先添加页面级别的字符 + if page.pdf_character: + chars.extend(page.pdf_character) + # 然后添加段落中的字符 + for paragraph in page.pdf_paragraph: + chars.extend(self.render_paragraph_to_char(paragraph)) + + # 渲染所有字符 + for char in chars: + if char.char_unicode == "\n": + continue + if char.pdf_character_id is None: + # dummy char + continue + char_size = char.pdf_style.font_size + font_id = char.pdf_style.font_id + if char.xobj_id in xobj_available_fonts: + if font_id not in xobj_available_fonts[char.xobj_id]: + continue + draw_op = xobj_draw_ops[char.xobj_id] + encoding_length_map = xobj_encoding_length_map[char.xobj_id] + else: + if font_id not in available_font_list: + continue + draw_op = page_op + encoding_length_map = page_encoding_length_map + + draw_op.append(b"q ") + self.render_graphic_state(draw_op, char.pdf_style.graphic_state) + if char.vertical: + draw_op.append( + f"BT /{font_id} {char_size:f} Tf 0 1 -1 0 {char.box.x2:f} {char.box.y:f} Tm ".encode() + ) + else: + draw_op.append( + f"BT /{font_id} {char_size:f} Tf 1 0 0 1 {char.box.x:f} {char.box.y:f} Tm ".encode() + ) + + encoding_length = encoding_length_map[font_id] + # pdf32000-2008 page14: + # As hexadecimal data enclosed in angle brackets < > + # see 7.3.4.3, "Hexadecimal Strings." + draw_op.append( + f"<{char.pdf_character_id:0{encoding_length * 2}x}>".upper().encode() + ) + + draw_op.append(b" Tj ET Q \n") + for xobj in page.pdf_xobject: + draw_op = xobj_draw_ops[xobj.xobj_id] + pdf.update_stream(xobj.xref_id, draw_op.tobytes()) + # pdf.update_stream(xobj.xref_id, b'') + for rect in page.pdf_rectangle: + self._debug_render_rectangle(page_op, rect) + draw_op = page_op + op_container = pdf.get_new_xref() + # Since this is a draw instruction container, + # no additional information is needed + pdf.update_object(op_container, "<<>>") + pdf.update_stream(op_container, draw_op.tobytes()) + pdf[page.page_number].set_contents(op_container) + pbar.advance() + translation_config.raise_if_cancelled() + with self.translation_config.progress_monitor.stage_start( + SUBSET_FONT_STAGE_NAME, 1 + ) as pbar: + if not translation_config.skip_clean: + pdf.subset_fonts(fallback=False) + pbar.advance() + with self.translation_config.progress_monitor.stage_start( + SAVE_PDF_STAGE_NAME, 2 + ) as pbar: + if not translation_config.no_mono: + if translation_config.debug: + translation_config.raise_if_cancelled() + pdf.save( + f"{mono_out_path}.decompressed.pdf", expand=True, pretty=True + ) + translation_config.raise_if_cancelled() + pdf.save( + mono_out_path, + garbage=3, + deflate=True, + clean=not translation_config.skip_clean, + deflate_fonts=True, + linear=True, + ) + pbar.advance() + dual_out_path = None + if not translation_config.no_dual: + dual_out_path = translation_config.get_output_file_path( + f"{basename}{debug_suffix}.{translation_config.lang_out}.dual.pdf" + ) + translation_config.raise_if_cancelled() + dual = pymupdf.open(self.original_pdf_path) + if translation_config.debug: + translation_config.raise_if_cancelled() + try: + self.write_debug_info(dual, translation_config) + except Exception: + logger.warning( + "Failed to write debug info to dual PDF", exc_info=True + ) + dual.insert_file(pdf) + page_count = pdf.page_count + for page_id in range(page_count): + if translation_config.dual_translate_first: + dual.move_page(page_count + page_id, page_id * 2) + else: + dual.move_page(page_count + page_id, page_id * 2 + 1) + dual.save( + dual_out_path, + garbage=3, + deflate=True, + clean=not translation_config.skip_clean, + deflate_fonts=True, + linear=True, + ) + if translation_config.debug: + translation_config.raise_if_cancelled() + dual.save( + f"{dual_out_path}.decompressed.pdf", expand=True, pretty=True + ) + pbar.advance() + return TranslateResult(mono_out_path, dual_out_path) diff --git a/src/pdf2u/document_il/frontend/__init__.py b/src/pdf2u/document_il/frontend/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/pdf2u/document_il/frontend/__pycache__/__init__.cpython-311.pyc b/src/pdf2u/document_il/frontend/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8be08c849610eade01ce3284d0297e63a533b621 Binary files /dev/null and b/src/pdf2u/document_il/frontend/__pycache__/__init__.cpython-311.pyc differ diff --git a/src/pdf2u/document_il/frontend/__pycache__/__init__.cpython-312.pyc b/src/pdf2u/document_il/frontend/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..46996116e58cda6ee0b1e836d09e3e2a8c03a868 Binary files /dev/null and b/src/pdf2u/document_il/frontend/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/pdf2u/document_il/frontend/__pycache__/il_creater.cpython-311.pyc b/src/pdf2u/document_il/frontend/__pycache__/il_creater.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..04931557c50f93c79defb6984958f2c67f0f9672 Binary files /dev/null and b/src/pdf2u/document_il/frontend/__pycache__/il_creater.cpython-311.pyc differ diff --git a/src/pdf2u/document_il/frontend/__pycache__/il_creater.cpython-312.pyc b/src/pdf2u/document_il/frontend/__pycache__/il_creater.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..848900736619edc990cde8e2383d3da80f249a94 Binary files /dev/null and b/src/pdf2u/document_il/frontend/__pycache__/il_creater.cpython-312.pyc differ diff --git a/src/pdf2u/document_il/frontend/il_creater.py b/src/pdf2u/document_il/frontend/il_creater.py new file mode 100644 index 0000000000000000000000000000000000000000..4547f03b68aeac093440158572cf95c7e3d3073f --- /dev/null +++ b/src/pdf2u/document_il/frontend/il_creater.py @@ -0,0 +1,328 @@ +import base64 +import logging +import re + +import pdfminer.pdfinterp +import pymupdf +from pdfminer.layout import LTChar, LTFigure +from pdfminer.pdffont import PDFCIDFont, PDFFont +from pdfminer.psparser import PSLiteral + +from pdf2u.document_il import il_version_1 +from pdf2u.translation_config import TranslationConfig + +logger = logging.getLogger(__name__) + + +class ILCreater: + stage_name = "Parse PDF and Create Intermediate Representation" + + def __init__(self, translation_config: TranslationConfig): + self.progress = None + self.current_page: il_version_1.Page = None + self.mupdf: pymupdf.Document = None + self.model = translation_config.doc_layout_model + self.docs = il_version_1.Document(page=[]) + self.stroking_color_space_name = None + self.non_stroking_color_space_name = None + self.passthrough_per_char_instruction: list[tuple[str, str]] = [] + self.translation_config = translation_config + self.passthrough_per_char_instruction_stack: list[list[tuple[str, str]]] = [] + self.xobj_id = 0 + self.xobj_inc = 0 + self.xobj_map: dict[int, il_version_1.PdfXobject] = {} + self.xobj_stack = [] + + def on_finish(self): + self.progress.__exit__(None, None, None) + + def is_passthrough_per_char_operation(self, operator: str): + return re.match("^(sc|scn|g|rg|k|cs|gs|ri)$", operator, re.IGNORECASE) + + def on_passthrough_per_char(self, operator: str, args: list[str]): + if not self.is_passthrough_per_char_operation(operator): + logger.error("Unknown passthrough_per_char operation: %s", operator) + return + # logger.debug("xobj_id: %d, on_passthrough_per_char: %s ( %s )", self.xobj_id, operator, args) + args = [self.parse_arg(arg) for arg in args] + for _i, value in enumerate(self.passthrough_per_char_instruction.copy()): + op, arg = value + if op == operator: + self.passthrough_per_char_instruction.remove(value) + break + self.passthrough_per_char_instruction.append((operator, " ".join(args))) + pass + + def remove_latest_passthrough_per_char_instruction(self): + if self.passthrough_per_char_instruction: + self.passthrough_per_char_instruction.pop() + + def parse_arg(self, arg: str): + if isinstance(arg, PSLiteral): + return f"/{arg.name}" + if not isinstance(arg, str): + return str(arg) + return arg + + def pop_passthrough_per_char_instruction(self): + if self.passthrough_per_char_instruction_stack: + self.passthrough_per_char_instruction = ( + self.passthrough_per_char_instruction_stack.pop() + ) + else: + self.passthrough_per_char_instruction = [] + logging.error( + "pop_passthrough_per_char_instruction error on page: %s", + self.current_page.page_number, + ) + + def push_passthrough_per_char_instruction(self): + self.passthrough_per_char_instruction_stack.append( + self.passthrough_per_char_instruction.copy() + ) + + # pdf32000 page 171 + def on_stroking_color_space(self, color_space_name): + self.stroking_color_space_name = color_space_name + + def on_non_stroking_color_space(self, color_space_name): + self.non_stroking_color_space_name = color_space_name + + def on_new_stream(self): + self.stroking_color_space_name = None + self.non_stroking_color_space_name = None + self.passthrough_per_char_instruction = [] + + def push_xobj(self): + self.xobj_stack.append( + (self.current_page_font_name_id_map.copy(), self.xobj_id) + ) + self.current_page_font_name_id_map = {} + + def pop_xobj(self): + self.current_page_font_name_id_map, self.xobj_id = self.xobj_stack.pop() + + def on_xobj_begin(self, bbox, xref_id): + self.push_passthrough_per_char_instruction() + self.push_xobj() + self.xobj_inc += 1 + self.xobj_id = self.xobj_inc + xobject = il_version_1.PdfXobject( + box=il_version_1.Box( + x=float(bbox[0]), y=float(bbox[1]), x2=float(bbox[2]), y2=float(bbox[3]) + ), + xobj_id=self.xobj_id, + xref_id=xref_id, + ) + self.current_page.pdf_xobject.append(xobject) + self.xobj_map[self.xobj_id] = xobject + return self.xobj_id + + def on_xobj_end(self, xobj_id, base_op): + self.pop_passthrough_per_char_instruction() + self.pop_xobj() + xobj = self.xobj_map[xobj_id] + xobj.base_operations = il_version_1.BaseOperations(value=base_op) + self.xobj_inc += 1 + + def on_page_start(self): + self.current_page = il_version_1.Page( + pdf_font=[], + pdf_character=[], + page_layout=[], + # currently don't support UserUnit page parameter + # pdf32000 page 79 + unit="point", + ) + self.current_page_font_name_id_map = {} + self.passthrough_per_char_instruction_stack = [] + self.xobj_stack = [] + self.non_stroking_color_space_name = None + self.stroking_color_space_name = None + self.docs.page.append(self.current_page) + + def on_page_end(self): + self.progress.advance(1) + + def on_page_crop_box( + self, x0: float | int, y0: float | int, x1: float | int, y1: float | int + ): + box = il_version_1.Box(x=float(x0), y=float(y0), x2=float(x1), y2=float(y1)) + self.current_page.cropbox = il_version_1.Cropbox(box=box) + + def on_page_media_box( + self, x0: float | int, y0: float | int, x1: float | int, y1: float | int + ): + box = il_version_1.Box(x=float(x0), y=float(y0), x2=float(x1), y2=float(y1)) + self.current_page.mediabox = il_version_1.Mediabox(box=box) + + def on_page_number(self, page_number: int): + assert isinstance(page_number, int) + assert page_number >= 0 + self.current_page.page_number = page_number + + def on_page_base_operation(self, operation: str): + self.current_page.base_operations = il_version_1.BaseOperations(value=operation) + + def on_page_resource_font(self, font: PDFFont, xref_id: int, font_id: str): + font_name = font.fontname + if isinstance(font_name, bytes): + try: + font_name = font_name.decode("utf-8") + except UnicodeDecodeError: + font_name = "BASE64:" + base64.b64encode(font_name).decode("utf-8") + encoding_length = 1 + if isinstance(font, PDFCIDFont): + try: + # pdf 32000:2008 page 273 + # Table 118 - Predefined CJK CMap names + _, encoding = self.mupdf.xref_get_key(xref_id, "Encoding") + if encoding == "/Identity-H" or encoding == "/Identity-V": + encoding_length = 2 + else: + _, to_unicode_id = self.mupdf.xref_get_key(xref_id, "ToUnicode") + to_unicode_bytes = self.mupdf.xref_stream( + int(to_unicode_id.split(" ")[0]) + ) + code_range = re.search( + b"begincodespacerange\n?.*<(\\d+?)>.*", to_unicode_bytes + ).group(1) + encoding_length = len(code_range) // 2 + except Exception: + if max(font.unicode_map.cid2unichr.keys()) > 255: + encoding_length = 2 + else: + encoding_length = 1 + try: + mupdf_font = pymupdf.Font(fontbuffer=self.mupdf.extract_font(xref_id)[3]) + bold = mupdf_font.is_bold + italic = mupdf_font.is_italic + monospaced = mupdf_font.is_monospaced + serif = mupdf_font.is_serif + except Exception: + bold = None + italic = None + monospaced = None + serif = None + il_font_metadata = il_version_1.PdfFont( + name=font_name, + xref_id=xref_id, + font_id=font_id, + encoding_length=encoding_length, + bold=bold, + italic=italic, + monospace=monospaced, + serif=serif, + ascent=font.ascent, + descent=font.descent, + ) + self.current_page_font_name_id_map[font_name] = font_id + if self.xobj_id in self.xobj_map: + self.xobj_map[self.xobj_id].pdf_font.append(il_font_metadata) + else: + self.current_page.pdf_font.append(il_font_metadata) + + def create_graphic_state(self, gs: pdfminer.pdfinterp.PDFGraphicState): + graphic_state = il_version_1.GraphicState() + for k, v in gs.__dict__.items(): + if v is None: + continue + if k in ["scolor", "ncolor"]: + if isinstance(v, tuple): + v = list(v) + else: + v = [v] + setattr(graphic_state, k, v) + continue + if k == "linewidth": + graphic_state.linewidth = float(v) + continue + continue + raise NotImplementedError + + graphic_state.stroking_color_space_name = self.stroking_color_space_name + graphic_state.non_stroking_color_space_name = self.non_stroking_color_space_name + + graphic_state.passthrough_per_char_instruction = " ".join( + f"{arg} {op}" for op, arg in gs.passthrough_instruction + ) + + return graphic_state + + def on_lt_char(self, char: LTChar): + gs = self.create_graphic_state(char.graphicstate) + # Get font from current page or xobject + font = None + for pdf_font in self.xobj_map.get(self.xobj_id, self.current_page).pdf_font: + if pdf_font.font_id == char.aw_font_id: + font = pdf_font + break + + # Get descent from font + descent = 0 + if font and hasattr(font, "descent"): + descent = font.descent * char.size / 1000 + + char_id = char.cid + char_unicode = char.get_text() + if "(cid:" not in char_unicode and len(char_unicode) > 1: + return + advance = char.adv + if char.matrix[0] == 0 and char.matrix[3] == 0: + vertical = True + bbox = il_version_1.Box( + x=char.bbox[0] - descent, + y=char.bbox[1], + x2=char.bbox[2] - descent, + y2=char.bbox[3], + ) + else: + vertical = False + # Add descent to y coordinates + bbox = il_version_1.Box( + x=char.bbox[0], + y=char.bbox[1] + descent, + x2=char.bbox[2], + y2=char.bbox[3] + descent, + ) + pdf_style = il_version_1.PdfStyle( + font_id=char.aw_font_id, font_size=char.size, graphic_state=gs + ) + pdf_char = il_version_1.PdfCharacter( + box=bbox, + pdf_character_id=char_id, + advance=advance, + char_unicode=char_unicode, + vertical=vertical, + pdf_style=pdf_style, + xobj_id=char.xobj_id, + ) + self.current_page.pdf_character.append(pdf_char) + + def create_il(self): + pages = [ + page + for page in self.docs.page + if self.translation_config.should_translate_page(page.page_number + 1) + ] + self.docs.page = pages + return self.docs + + def on_total_pages(self, total_pages: int): + assert isinstance(total_pages, int) + assert total_pages > 0 + self.docs.total_pages = total_pages + total = 0 + for page in range(total_pages): + if self.translation_config.should_translate_page(page + 1) is False: + continue + total += 1 + self.progress = self.translation_config.progress_monitor.stage_start( + self.stage_name, total + ) + + def on_pdf_figure(self, figure: LTFigure): + box = il_version_1.Box( + figure.bbox[0], figure.bbox[1], figure.bbox[2], figure.bbox[3] + ) + self.current_page.pdf_figure.append(il_version_1.PdfFigure(box=box)) diff --git a/src/pdf2u/document_il/il_version_1.py b/src/pdf2u/document_il/il_version_1.py new file mode 100644 index 0000000000000000000000000000000000000000..d47ed46ece11514558c249d458303670c2e26a91 --- /dev/null +++ b/src/pdf2u/document_il/il_version_1.py @@ -0,0 +1,396 @@ +from dataclasses import dataclass, field + + +@dataclass +class BaseOperations: + class Meta: + name = "baseOperations" + + value: str = field(default="", metadata={"required": True}) + + +@dataclass +class Box: + class Meta: + name = "box" + + x: float | None = field( + default=None, metadata={"type": "Attribute", "required": True} + ) + y: float | None = field( + default=None, metadata={"type": "Attribute", "required": True} + ) + x2: float | None = field( + default=None, metadata={"type": "Attribute", "required": True} + ) + y2: float | None = field( + default=None, metadata={"type": "Attribute", "required": True} + ) + + +@dataclass +class GraphicState: + class Meta: + name = "graphicState" + + linewidth: float | None = field(default=None, metadata={"type": "Attribute"}) + dash: list[float] = field( + default_factory=list, + metadata={"type": "Attribute", "min_length": 1, "tokens": True}, + ) + flatness: float | None = field(default=None, metadata={"type": "Attribute"}) + intent: str | None = field(default=None, metadata={"type": "Attribute"}) + linecap: int | None = field(default=None, metadata={"type": "Attribute"}) + linejoin: int | None = field(default=None, metadata={"type": "Attribute"}) + miterlimit: float | None = field(default=None, metadata={"type": "Attribute"}) + ncolor: list[float] = field( + default_factory=list, + metadata={"type": "Attribute", "min_length": 1, "tokens": True}, + ) + scolor: list[float] = field( + default_factory=list, + metadata={"type": "Attribute", "min_length": 1, "tokens": True}, + ) + stroking_color_space_name: str | None = field( + default=None, metadata={"name": "strokingColorSpaceName", "type": "Attribute"} + ) + non_stroking_color_space_name: str | None = field( + default=None, + metadata={"name": "nonStrokingColorSpaceName", "type": "Attribute"}, + ) + passthrough_per_char_instruction: str | None = field( + default=None, + metadata={"name": "passthroughPerCharInstruction", "type": "Attribute"}, + ) + + +@dataclass +class PdfFont: + class Meta: + name = "pdfFont" + + name: str | None = field( + default=None, metadata={"type": "Attribute", "required": True} + ) + font_id: str | None = field( + default=None, metadata={"name": "fontId", "type": "Attribute", "required": True} + ) + xref_id: int | None = field( + default=None, metadata={"name": "xrefId", "type": "Attribute", "required": True} + ) + encoding_length: int | None = field( + default=None, + metadata={"name": "encodingLength", "type": "Attribute", "required": True}, + ) + bold: bool | None = field(default=None, metadata={"type": "Attribute"}) + italic: bool | None = field(default=None, metadata={"type": "Attribute"}) + monospace: bool | None = field(default=None, metadata={"type": "Attribute"}) + serif: bool | None = field(default=None, metadata={"type": "Attribute"}) + ascent: float | None = field(default=None, metadata={"type": "Attribute"}) + descent: float | None = field(default=None, metadata={"type": "Attribute"}) + + +@dataclass +class Cropbox: + class Meta: + name = "cropbox" + + box: Box | None = field( + default=None, metadata={"type": "Element", "required": True} + ) + + +@dataclass +class Mediabox: + class Meta: + name = "mediabox" + + box: Box | None = field( + default=None, metadata={"type": "Element", "required": True} + ) + + +@dataclass +class PageLayout: + class Meta: + name = "pageLayout" + + box: Box | None = field( + default=None, metadata={"type": "Element", "required": True} + ) + id: int | None = field( + default=None, metadata={"type": "Attribute", "required": True} + ) + conf: float | None = field( + default=None, metadata={"type": "Attribute", "required": True} + ) + class_name: str | None = field( + default=None, metadata={"type": "Attribute", "required": True} + ) + + +@dataclass +class PdfFigure: + class Meta: + name = "pdfFigure" + + box: Box | None = field( + default=None, metadata={"type": "Element", "required": True} + ) + + +@dataclass +class PdfRectangle: + class Meta: + name = "pdfRectangle" + + box: Box | None = field( + default=None, metadata={"type": "Element", "required": True} + ) + graphic_state: GraphicState | None = field( + default=None, + metadata={"name": "graphicState", "type": "Element", "required": True}, + ) + debug_info: bool | None = field(default=None, metadata={"type": "Attribute"}) + + +@dataclass +class PdfStyle: + class Meta: + name = "pdfStyle" + + graphic_state: GraphicState | None = field( + default=None, + metadata={"name": "graphicState", "type": "Element", "required": True}, + ) + font_id: str | None = field( + default=None, metadata={"type": "Attribute", "required": True} + ) + font_size: float | None = field( + default=None, metadata={"type": "Attribute", "required": True} + ) + + +@dataclass +class PdfXobject: + class Meta: + name = "pdfXobject" + + box: Box | None = field( + default=None, metadata={"type": "Element", "required": True} + ) + pdf_font: list[PdfFont] = field( + default_factory=list, metadata={"name": "pdfFont", "type": "Element"} + ) + base_operations: BaseOperations | None = field( + default=None, + metadata={"name": "baseOperations", "type": "Element", "required": True}, + ) + xobj_id: int | None = field( + default=None, metadata={"name": "xobjId", "type": "Attribute", "required": True} + ) + xref_id: int | None = field( + default=None, metadata={"name": "xrefId", "type": "Attribute", "required": True} + ) + + +@dataclass +class PdfCharacter: + class Meta: + name = "pdfCharacter" + + pdf_style: PdfStyle | None = field( + default=None, metadata={"name": "pdfStyle", "type": "Element", "required": True} + ) + box: Box | None = field( + default=None, metadata={"type": "Element", "required": True} + ) + vertical: bool | None = field(default=None, metadata={"type": "Attribute"}) + scale: float | None = field(default=None, metadata={"type": "Attribute"}) + pdf_character_id: int | None = field( + default=None, metadata={"name": "pdfCharacterId", "type": "Attribute"} + ) + char_unicode: str | None = field( + default=None, metadata={"type": "Attribute", "required": True} + ) + advance: float | None = field(default=None, metadata={"type": "Attribute"}) + xobj_id: int | None = field( + default=None, metadata={"name": "xobjId", "type": "Attribute"} + ) + debug_info: bool | None = field(default=None, metadata={"type": "Attribute"}) + + +@dataclass +class PdfSameStyleUnicodeCharacters: + class Meta: + name = "pdfSameStyleUnicodeCharacters" + + pdf_style: PdfStyle | None = field( + default=None, metadata={"name": "pdfStyle", "type": "Element"} + ) + unicode: str | None = field( + default=None, metadata={"type": "Attribute", "required": True} + ) + debug_info: bool | None = field(default=None, metadata={"type": "Attribute"}) + + +@dataclass +class PdfFormula: + class Meta: + name = "pdfFormula" + + box: Box | None = field( + default=None, metadata={"type": "Element", "required": True} + ) + pdf_character: list[PdfCharacter] = field( + default_factory=list, + metadata={"name": "pdfCharacter", "type": "Element", "min_occurs": 1}, + ) + x_offset: float | None = field( + default=None, metadata={"type": "Attribute", "required": True} + ) + y_offset: float | None = field( + default=None, metadata={"type": "Attribute", "required": True} + ) + + +@dataclass +class PdfLine: + class Meta: + name = "pdfLine" + + box: Box | None = field( + default=None, metadata={"type": "Element", "required": True} + ) + pdf_character: list[PdfCharacter] = field( + default_factory=list, + metadata={"name": "pdfCharacter", "type": "Element", "min_occurs": 1}, + ) + + +@dataclass +class PdfSameStyleCharacters: + class Meta: + name = "pdfSameStyleCharacters" + + box: Box | None = field( + default=None, metadata={"type": "Element", "required": True} + ) + pdf_style: PdfStyle | None = field( + default=None, metadata={"name": "pdfStyle", "type": "Element", "required": True} + ) + pdf_character: list[PdfCharacter] = field( + default_factory=list, + metadata={"name": "pdfCharacter", "type": "Element", "min_occurs": 1}, + ) + + +@dataclass +class PdfParagraphComposition: + class Meta: + name = "pdfParagraphComposition" + + pdf_line: PdfLine | None = field( + default=None, metadata={"name": "pdfLine", "type": "Element"} + ) + pdf_formula: PdfFormula | None = field( + default=None, metadata={"name": "pdfFormula", "type": "Element"} + ) + pdf_same_style_characters: PdfSameStyleCharacters | None = field( + default=None, metadata={"name": "pdfSameStyleCharacters", "type": "Element"} + ) + pdf_character: PdfCharacter | None = field( + default=None, metadata={"name": "pdfCharacter", "type": "Element"} + ) + pdf_same_style_unicode_characters: PdfSameStyleUnicodeCharacters | None = field( + default=None, + metadata={"name": "pdfSameStyleUnicodeCharacters", "type": "Element"}, + ) + + +@dataclass +class PdfParagraph: + class Meta: + name = "pdfParagraph" + + box: Box | None = field( + default=None, metadata={"type": "Element", "required": True} + ) + pdf_style: PdfStyle | None = field( + default=None, metadata={"name": "pdfStyle", "type": "Element", "required": True} + ) + pdf_paragraph_composition: list[PdfParagraphComposition] = field( + default_factory=list, + metadata={"name": "pdfParagraphComposition", "type": "Element"}, + ) + xobj_id: int | None = field( + default=None, metadata={"name": "xobjId", "type": "Attribute"} + ) + unicode: str | None = field( + default=None, metadata={"type": "Attribute", "required": True} + ) + scale: float | None = field(default=None, metadata={"type": "Attribute"}) + vertical: bool | None = field(default=None, metadata={"type": "Attribute"}) + first_line_indent: bool | None = field( + default=None, metadata={"name": "FirstLineIndent", "type": "Attribute"} + ) + debug_id: str | None = field(default=None, metadata={"type": "Attribute"}) + + +@dataclass +class Page: + class Meta: + name = "page" + + mediabox: Mediabox | None = field( + default=None, metadata={"type": "Element", "required": True} + ) + cropbox: Cropbox | None = field( + default=None, metadata={"type": "Element", "required": True} + ) + pdf_xobject: list[PdfXobject] = field( + default_factory=list, metadata={"name": "pdfXobject", "type": "Element"} + ) + page_layout: list[PageLayout] = field( + default_factory=list, metadata={"name": "pageLayout", "type": "Element"} + ) + pdf_rectangle: list[PdfRectangle] = field( + default_factory=list, metadata={"name": "pdfRectangle", "type": "Element"} + ) + pdf_font: list[PdfFont] = field( + default_factory=list, metadata={"name": "pdfFont", "type": "Element"} + ) + pdf_paragraph: list[PdfParagraph] = field( + default_factory=list, metadata={"name": "pdfParagraph", "type": "Element"} + ) + pdf_figure: list[PdfFigure] = field( + default_factory=list, metadata={"name": "pdfFigure", "type": "Element"} + ) + pdf_character: list[PdfCharacter] = field( + default_factory=list, metadata={"name": "pdfCharacter", "type": "Element"} + ) + base_operations: BaseOperations | None = field( + default=None, + metadata={"name": "baseOperations", "type": "Element", "required": True}, + ) + page_number: int | None = field( + default=None, + metadata={"name": "pageNumber", "type": "Attribute", "required": True}, + ) + unit: str | None = field( + default=None, metadata={"name": "Unit", "type": "Attribute", "required": True} + ) + + +@dataclass +class Document: + class Meta: + name = "document" + + page: list[Page] = field( + default_factory=list, metadata={"type": "Element", "min_occurs": 1} + ) + total_pages: int | None = field( + default=None, + metadata={"name": "totalPages", "type": "Attribute", "required": True}, + ) diff --git a/src/pdf2u/document_il/il_version_1.rnc b/src/pdf2u/document_il/il_version_1.rnc new file mode 100644 index 0000000000000000000000000000000000000000..de69bb48eaa1951a78400ee9baf30dbc6790d097 --- /dev/null +++ b/src/pdf2u/document_il/il_version_1.rnc @@ -0,0 +1,141 @@ +start = Document +Document = + element document { + Page+, + attribute totalPages { xsd:int } + } +Page = + element page { + element mediabox { Box }, + element cropbox { Box }, + PDFXobject*, + PageLayout*, + PDFRectangle*, + PDFFont*, + PDFParagraph*, + PDFFigure*, + PDFCharacter*, + attribute pageNumber { xsd:int }, + attribute Unit { xsd:string }, + element baseOperations { xsd:string } + } +Box = + element box { + # from (x,y) to (x2,y2) + attribute x { xsd:float }, + attribute y { xsd:float }, + attribute x2 { xsd:float }, + attribute y2 { xsd:float } + } +PDFXrefId = xsd:int +PDFFont = + element pdfFont { + attribute name { xsd:string }, + attribute fontId { xsd:string }, + attribute xrefId { PDFXrefId }, + attribute encodingLength { xsd:int }, + attribute bold { xsd:boolean }?, + attribute italic { xsd:boolean }?, + attribute monospace { xsd:boolean }?, + attribute serif { xsd:boolean }?, + attribute ascent { xsd:float }?, + attribute descent { xsd:float }? + } +PDFXobject = + element pdfXobject { + attribute xobjId { xsd:int }, + attribute xrefId { PDFXrefId }, + Box, + PDFFont*, + element baseOperations { xsd:string } + } +PDFCharacter = + element pdfCharacter { + attribute vertical { xsd:boolean }?, + attribute scale { xsd:float }?, + attribute pdfCharacterId { xsd:int }?, + attribute char_unicode { xsd:string }, + attribute advance { xsd:float }?, + # xobject nesting depth + attribute xobjId { xsd:int }?, + attribute debug_info { xsd:boolean }?, + PDFStyle, + Box + } +PageLayout = + element pageLayout { + attribute id { xsd:int }, + attribute conf { xsd:float }, + attribute class_name { xsd:string }, + Box + } +GraphicState = + element graphicState { + attribute linewidth { xsd:float }?, + attribute dash { + list { xsd:float+ } + }?, + attribute flatness { xsd:float }?, + attribute intent { xsd:string }?, + attribute linecap { xsd:int }?, + attribute linejoin { xsd:int }?, + attribute miterlimit { xsd:float }?, + attribute ncolor { + list { xsd:float+ } + }?, + attribute scolor { + list { xsd:float+ } + }?, + attribute strokingColorSpaceName { xsd:string }?, + attribute nonStrokingColorSpaceName { xsd:string }?, + attribute passthroughPerCharInstruction { xsd:string }? + } +PDFStyle = + element pdfStyle { + attribute font_id { xsd:string }, + attribute font_size { xsd:float }, + GraphicState + } +PDFParagraph = + element pdfParagraph { + attribute xobjId { xsd:int }?, + attribute unicode { xsd:string }, + attribute scale { xsd:float }?, + attribute vertical { xsd:boolean }?, + attribute FirstLineIndent { xsd:boolean }?, + attribute debug_id { xsd:string }?, + Box, + PDFStyle, + PDFParagraphComposition* + } +PDFParagraphComposition = + element pdfParagraphComposition { + PDFLine + | PDFFormula + | PDFSameStyleCharacters + | PDFCharacter + | PDFSameStyleUnicodeCharacters + } +PDFLine = element pdfLine { Box, PDFCharacter+ } +PDFSameStyleCharacters = + element pdfSameStyleCharacters { Box, PDFStyle, PDFCharacter+ } +PDFSameStyleUnicodeCharacters = + element pdfSameStyleUnicodeCharacters { + PDFStyle?, + attribute unicode { xsd:string }, + attribute debug_info { xsd:boolean }? + } +PDFFormula = + element pdfFormula { + Box, + PDFCharacter+, + attribute x_offset { xsd:float }, + attribute y_offset { xsd:float } + } +PDFFigure = element pdfFigure { Box } +PDFRectangle = + element pdfRectangle { + Box, + GraphicState, + attribute debug_info { xsd:boolean }? + } diff --git a/src/pdf2u/document_il/il_version_1.rng b/src/pdf2u/document_il/il_version_1.rng new file mode 100644 index 0000000000000000000000000000000000000000..ec838dca0cb5c158bcfd34a449307c5762ae49d8 --- /dev/null +++ b/src/pdf2u/document_il/il_version_1.rng @@ -0,0 +1,390 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pdf2u/document_il/il_version_1.xsd b/src/pdf2u/document_il/il_version_1.xsd new file mode 100644 index 0000000000000000000000000000000000000000..0c6000ff39fc8ec4aeb7d13d72aa60638631ff96 --- /dev/null +++ b/src/pdf2u/document_il/il_version_1.xsd @@ -0,0 +1,235 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pdf2u/document_il/midend/__init__.py b/src/pdf2u/document_il/midend/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/pdf2u/document_il/midend/__pycache__/__init__.cpython-311.pyc b/src/pdf2u/document_il/midend/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..13ebb30c9790fef5653050fa74ce259fbe14576d Binary files /dev/null and b/src/pdf2u/document_il/midend/__pycache__/__init__.cpython-311.pyc differ diff --git a/src/pdf2u/document_il/midend/__pycache__/__init__.cpython-312.pyc b/src/pdf2u/document_il/midend/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5d4366fe5a565d204b31e45c0c9f9894d60addf3 Binary files /dev/null and b/src/pdf2u/document_il/midend/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/pdf2u/document_il/midend/__pycache__/add_debug_information.cpython-311.pyc b/src/pdf2u/document_il/midend/__pycache__/add_debug_information.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3e833dcb005e15fc84a89e43311197b72db390e3 Binary files /dev/null and b/src/pdf2u/document_il/midend/__pycache__/add_debug_information.cpython-311.pyc differ diff --git a/src/pdf2u/document_il/midend/__pycache__/add_debug_information.cpython-312.pyc b/src/pdf2u/document_il/midend/__pycache__/add_debug_information.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5abf77374c7326662598d4883225ceda182c211f Binary files /dev/null and b/src/pdf2u/document_il/midend/__pycache__/add_debug_information.cpython-312.pyc differ diff --git a/src/pdf2u/document_il/midend/__pycache__/il_translator.cpython-311.pyc b/src/pdf2u/document_il/midend/__pycache__/il_translator.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8bb393d57e96340ef92949e237cb3b8f80b6890c Binary files /dev/null and b/src/pdf2u/document_il/midend/__pycache__/il_translator.cpython-311.pyc differ diff --git a/src/pdf2u/document_il/midend/__pycache__/il_translator.cpython-312.pyc b/src/pdf2u/document_il/midend/__pycache__/il_translator.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7473f6068938d18ae1824302a56e3cfdfe5d1417 Binary files /dev/null and b/src/pdf2u/document_il/midend/__pycache__/il_translator.cpython-312.pyc differ diff --git a/src/pdf2u/document_il/midend/__pycache__/layout_parser.cpython-311.pyc b/src/pdf2u/document_il/midend/__pycache__/layout_parser.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6534c4113f1a5e551ee47a2f633228a4d53cc015 Binary files /dev/null and b/src/pdf2u/document_il/midend/__pycache__/layout_parser.cpython-311.pyc differ diff --git a/src/pdf2u/document_il/midend/__pycache__/layout_parser.cpython-312.pyc b/src/pdf2u/document_il/midend/__pycache__/layout_parser.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..19320411d0bdf66b649b267551253390d2f7d44b Binary files /dev/null and b/src/pdf2u/document_il/midend/__pycache__/layout_parser.cpython-312.pyc differ diff --git a/src/pdf2u/document_il/midend/__pycache__/paragraph_finder.cpython-311.pyc b/src/pdf2u/document_il/midend/__pycache__/paragraph_finder.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4296508561f94725d4c886533f735e4c1072a7f1 Binary files /dev/null and b/src/pdf2u/document_il/midend/__pycache__/paragraph_finder.cpython-311.pyc differ diff --git a/src/pdf2u/document_il/midend/__pycache__/paragraph_finder.cpython-312.pyc b/src/pdf2u/document_il/midend/__pycache__/paragraph_finder.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..043fcde8dd7bb8e69757f82ab32423adb95de059 Binary files /dev/null and b/src/pdf2u/document_il/midend/__pycache__/paragraph_finder.cpython-312.pyc differ diff --git a/src/pdf2u/document_il/midend/__pycache__/remove_descent.cpython-311.pyc b/src/pdf2u/document_il/midend/__pycache__/remove_descent.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b843f812d0059eaa61014405d13005f70251e2ae Binary files /dev/null and b/src/pdf2u/document_il/midend/__pycache__/remove_descent.cpython-311.pyc differ diff --git a/src/pdf2u/document_il/midend/__pycache__/remove_descent.cpython-312.pyc b/src/pdf2u/document_il/midend/__pycache__/remove_descent.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c6b8220e02cfbf9d8807b0379afb70c46eb247b9 Binary files /dev/null and b/src/pdf2u/document_il/midend/__pycache__/remove_descent.cpython-312.pyc differ diff --git a/src/pdf2u/document_il/midend/__pycache__/styles_and_formulas.cpython-311.pyc b/src/pdf2u/document_il/midend/__pycache__/styles_and_formulas.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0a9a6155e2ab84d34686c1183545b552ccc7197b Binary files /dev/null and b/src/pdf2u/document_il/midend/__pycache__/styles_and_formulas.cpython-311.pyc differ diff --git a/src/pdf2u/document_il/midend/__pycache__/styles_and_formulas.cpython-312.pyc b/src/pdf2u/document_il/midend/__pycache__/styles_and_formulas.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5a1fc2679c2d94f8de35e92a871c8c3e65e3b05d Binary files /dev/null and b/src/pdf2u/document_il/midend/__pycache__/styles_and_formulas.cpython-312.pyc differ diff --git a/src/pdf2u/document_il/midend/__pycache__/typesetting.cpython-311.pyc b/src/pdf2u/document_il/midend/__pycache__/typesetting.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b156e93d926ce693a4c95262ab7d7c3d57553aba Binary files /dev/null and b/src/pdf2u/document_il/midend/__pycache__/typesetting.cpython-311.pyc differ diff --git a/src/pdf2u/document_il/midend/__pycache__/typesetting.cpython-312.pyc b/src/pdf2u/document_il/midend/__pycache__/typesetting.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f057f3c3d60918b89f24a89f5dcb8a5c20896c46 Binary files /dev/null and b/src/pdf2u/document_il/midend/__pycache__/typesetting.cpython-312.pyc differ diff --git a/src/pdf2u/document_il/midend/add_debug_information.py b/src/pdf2u/document_il/midend/add_debug_information.py new file mode 100644 index 0000000000000000000000000000000000000000..969a6ce15697c939a62492bbdc19d264a791e617 --- /dev/null +++ b/src/pdf2u/document_il/midend/add_debug_information.py @@ -0,0 +1,105 @@ +import logging + +import pdf2u.document_il.il_version_1 as il_version_1 +from pdf2u.document_il import GraphicState +from pdf2u.document_il.utils.style_helper import BLUE, ORANGE, YELLOW +from pdf2u.translation_config import TranslationConfig + +logger = logging.getLogger(__name__) + + +class AddDebugInformation: + stage_name = "Add Debug Information" + + def __init__(self, translation_config: TranslationConfig): + self.translation_config = translation_config + self.model = translation_config.doc_layout_model + + def process(self, docs: il_version_1.Document): + if not self.translation_config.debug: + return + + for page in docs.page: + self.process_page(page) + + def _create_rectangle(self, box: il_version_1.Box, color: GraphicState): + rect = il_version_1.PdfRectangle(box=box, graphic_state=color, debug_info=True) + return rect + + def _create_text(self, text: str, color: GraphicState, box: il_version_1.Box): + style = il_version_1.PdfStyle( + font_id="china-ss", font_size=4, graphic_state=color + ) + return il_version_1.PdfParagraph( + first_line_indent=False, + box=il_version_1.Box(x=box.x, y=box.y2, x2=box.x2, y2=box.y2 + 5), + vertical=False, + pdf_style=style, + unicode=text, + pdf_paragraph_composition=[ + il_version_1.PdfParagraphComposition( + pdf_same_style_unicode_characters=il_version_1.PdfSameStyleUnicodeCharacters( + unicode=text, pdf_style=style, debug_info=True + ) + ) + ], + xobj_id=-1, + ) + + def process_page(self, page: il_version_1.Page): + # Add page number text at top-left corner + page_width = page.cropbox.box.x2 - page.cropbox.box.x + page_height = page.cropbox.box.y2 - page.cropbox.box.y + page_number_text = f"pagenumber: {page.page_number}" + page_number_box = il_version_1.Box( + x=page.cropbox.box.x + page_width * 0.02, + y=page.cropbox.box.y, + x2=page.cropbox.box.x2, + y2=page.cropbox.box.y2 - page_height * 0.02, + ) + page_number_paragraph = self._create_text( + page_number_text, BLUE, page_number_box + ) + page.pdf_paragraph.append(page_number_paragraph) + + new_paragraphs = [] + + for paragraph in page.pdf_paragraph: + if not paragraph.pdf_paragraph_composition: + continue + if any( + x.pdf_same_style_unicode_characters.debug_info + for x in paragraph.pdf_paragraph_composition + if x.pdf_same_style_unicode_characters + ): + continue + # Create a rectangle box + rect = self._create_rectangle(paragraph.box, BLUE) + + page.pdf_rectangle.append(rect) + + # Create text label at top-left corner + # Note: PDF coordinates are from bottom-left, + # so we use y2 for top position + + debug_text = "paragraph" + if hasattr(paragraph, "debug_id") and paragraph.debug_id: + debug_text = f"paragraph[{paragraph.debug_id}]" + new_paragraphs.append(self._create_text(debug_text, BLUE, paragraph.box)) + + for composition in paragraph.pdf_paragraph_composition: + if composition.pdf_formula: + new_paragraphs.append( + self._create_text( + "formula", ORANGE, composition.pdf_formula.box + ) + ) + page.pdf_rectangle.append( + self._create_rectangle(composition.pdf_formula.box, ORANGE) + ) + + for xobj in page.pdf_xobject: + new_paragraphs.append(self._create_text("xobj", YELLOW, xobj.box)) + page.pdf_rectangle.append(self._create_rectangle(xobj.box, YELLOW)) + + page.pdf_paragraph.extend(new_paragraphs) diff --git a/src/pdf2u/document_il/midend/il_translator.py b/src/pdf2u/document_il/midend/il_translator.py new file mode 100644 index 0000000000000000000000000000000000000000..b49a693f41e46fffc3a66dd25a1391630abd6f08 --- /dev/null +++ b/src/pdf2u/document_il/midend/il_translator.py @@ -0,0 +1,550 @@ +import concurrent.futures +import json +import logging +from pathlib import Path + +from tqdm import tqdm + +from pdf2u.document_il import ( + Document, + Page, + PdfFont, + PdfFormula, + PdfParagraph, + PdfParagraphComposition, + PdfSameStyleCharacters, + PdfSameStyleUnicodeCharacters, + PdfStyle, +) +from pdf2u.document_il.translator.translator import BaseTranslator +from pdf2u.document_il.utils.fontmap import FontMapper +from pdf2u.document_il.utils.layout_helper import ( + get_char_unicode_string, + is_same_style, + is_same_style_except_font, + is_same_style_except_size, +) +from pdf2u.translation_config import TranslationConfig + +logger = logging.getLogger(__name__) + + +class RichTextPlaceholder: + def __init__( + self, + placeholder_id: int, + composition: PdfSameStyleCharacters, + left_placeholder: str, + right_placeholder: str, + ): + self.id = placeholder_id + self.composition = composition + self.left_placeholder = left_placeholder + self.right_placeholder = right_placeholder + + +class FormulaPlaceholder: + def __init__(self, placeholder_id: int, formula: PdfFormula, placeholder: str): + self.id = placeholder_id + self.formula = formula + self.placeholder = placeholder + + +class PbarContext: + def __init__(self, pbar): + self.pbar = pbar + + def __enter__(self): + return self.pbar + + def __exit__(self, exc_type, exc_value, traceback): + self.pbar.advance() + + +class DocumentTranslateTracker: + def __init__(self): + self.page = [] + + def new_page(self): + page = PageTranslateTracker() + self.page.append(page) + return page + + def to_json(self): + pages = [] + for page in self.page: + paragraphs = [] + for para in page.paragraph: + i_str = getattr(para, "input", None) + o_str = getattr(para, "output", None) + pdf_unicode = getattr(para, "pdf_unicode", None) + if pdf_unicode is None or i_str is None: + continue + paragraphs.append( + {"input": i_str, "output": o_str, "pdf_unicode": pdf_unicode} + ) + pages.append({"paragraph": paragraphs}) + return json.dumps({"page": pages}, ensure_ascii=False, indent=2) + + +class PageTranslateTracker: + def __init__(self): + self.paragraph = [] + + def new_paragraph(self): + paragraph = ParagraphTranslateTracker() + self.paragraph.append(paragraph) + return paragraph + + +class ParagraphTranslateTracker: + def __init__(self): + pass + + def set_pdf_unicode(self, unicode: str): + self.pdf_unicode = unicode + + def set_input(self, input_text: str): + self.input = input_text + + def set_output(self, output: str): + self.output = output + + +class ILTranslator: + stage_name = "Translate Paragraphs" + + def __init__( + self, translate_engine: BaseTranslator, translation_config: TranslationConfig + ): + self.translate_engine = translate_engine + self.translation_config = translation_config + self.font_mapper = FontMapper(translation_config) + + def translate(self, docs: Document): + tracker = DocumentTranslateTracker() + # count total paragraph + total = sum(len(page.pdf_paragraph) for page in docs.page) + with self.translation_config.progress_monitor.stage_start( + self.stage_name, total + ) as pbar: + with concurrent.futures.ThreadPoolExecutor( + max_workers=min( + self.translation_config.qps * 2, self.translation_config.qps + 5 + ) + ) as executor: + for page in docs.page: + self.process_page(page, executor, pbar, tracker.new_page()) + + path = self.translation_config.get_working_file_path("translate_tracking.json") + + if self.translation_config.debug: + logger.debug(f"save translate tracking to {path}") + with Path(path).open("w", encoding="utf-8") as f: + f.write(tracker.to_json()) + + def process_page( + self, + page: Page, + executor: concurrent.futures.ThreadPoolExecutor, + pbar: tqdm | None = None, + tracker: PageTranslateTracker = None, + ): + self.translation_config.raise_if_cancelled() + for paragraph in page.pdf_paragraph: + page_font_map = {} + for font in page.pdf_font: + page_font_map[font.font_id] = font + page_xobj_font_map = {} + for xobj in page.pdf_xobject: + page_xobj_font_map[xobj.xobj_id] = page_font_map.copy() + for font in xobj.pdf_font: + page_xobj_font_map[xobj.xobj_id][font.font_id] = font + # self.translate_paragraph(paragraph, pbar,tracker.new_paragraph(), page_font_map, page_xobj_font_map) + executor.submit( + self.translate_paragraph, + paragraph, + pbar, + tracker.new_paragraph(), + page_font_map, + page_xobj_font_map, + ) + + class TranslateInput: + def __init__( + self, + unicode: str, + placeholders: list[RichTextPlaceholder | FormulaPlaceholder], + base_style: PdfStyle = None, + ): + self.unicode = unicode + self.placeholders = placeholders + self.base_style = base_style + + def create_formula_placeholder( + self, formula: PdfFormula, formula_id: int, paragraph: PdfParagraph + ): + placeholder = self.translate_engine.get_formular_placeholder(formula_id) + if placeholder in paragraph.unicode: + return self.create_formula_placeholder(formula, formula_id + 1, paragraph) + + return FormulaPlaceholder(formula_id, formula, placeholder) + + def create_rich_text_placeholder( + self, + composition: PdfSameStyleCharacters, + composition_id: int, + paragraph: PdfParagraph, + ): + left_placeholder = self.translate_engine.get_rich_text_left_placeholder( + composition_id + ) + right_placeholder = self.translate_engine.get_rich_text_right_placeholder( + composition_id + ) + if ( + left_placeholder in paragraph.unicode + or right_placeholder in paragraph.unicode + ): + return self.create_rich_text_placeholder( + composition, composition_id + 1, paragraph + ) + + return RichTextPlaceholder( + composition_id, composition, left_placeholder, right_placeholder + ) + + def get_translate_input( + self, + paragraph: PdfParagraph, + page_font_map: dict[str, PdfFont] = None, + disable_rich_text_translate: bool | None = None, + ): + if not paragraph.pdf_paragraph_composition: + return + if len(paragraph.pdf_paragraph_composition) == 1: + # 如果整个段落只有一个组成部分,那么直接返回,不需要套占位符等 + composition = paragraph.pdf_paragraph_composition[0] + if ( + composition.pdf_line + or composition.pdf_same_style_characters + or composition.pdf_character + ): + return self.TranslateInput(paragraph.unicode, [], paragraph.pdf_style) + elif composition.pdf_formula: + # 不需要翻译纯公式 + return None + elif composition.pdf_same_style_unicode_characters: + # DEBUG INSERT CHAR, NOT TRANSLATE + return None + else: + logger.error( + f"Unknown composition type. " + f"Composition: {composition}. " + f"Paragraph: {paragraph}. " + ) + return None + + # 如果没有指定 disable_rich_text_translate,使用配置中的值 + if disable_rich_text_translate is None: + disable_rich_text_translate = ( + self.translation_config.disable_rich_text_translate + ) + + placeholder_id = 1 + placeholders = [] + chars = [] + for composition in paragraph.pdf_paragraph_composition: + if composition.pdf_line: + chars.extend(composition.pdf_line.pdf_character) + elif composition.pdf_formula: + formula_placeholder = self.create_formula_placeholder( + composition.pdf_formula, placeholder_id, paragraph + ) + placeholders.append(formula_placeholder) + # 公式只需要一个占位符,所以 id+1 + placeholder_id = formula_placeholder.id + 1 + chars.extend(formula_placeholder.placeholder) + elif composition.pdf_character: + chars.append(composition.pdf_character) + elif composition.pdf_same_style_characters: + if disable_rich_text_translate: + # 如果禁用富文本翻译,直接添加字符 + chars.extend(composition.pdf_same_style_characters.pdf_character) + continue + + fonta = self.font_mapper.map( + page_font_map[ + composition.pdf_same_style_characters.pdf_style.font_id + ], + "1", + ) + fontb = self.font_mapper.map( + page_font_map[paragraph.pdf_style.font_id], "1" + ) + if ( + # 样式和段落基准样式一致,无需占位符 + is_same_style( + composition.pdf_same_style_characters.pdf_style, + paragraph.pdf_style, + ) + # 字号差异在0.7-1.3之间,可能是首字母变大效果,无需占位符 + or is_same_style_except_size( + composition.pdf_same_style_characters.pdf_style, + paragraph.pdf_style, + ) + or ( + # 除了字体以外样式都和基准一样,并且字体都映射到同一个字体。无需占位符 + is_same_style_except_font( + composition.pdf_same_style_characters.pdf_style, + paragraph.pdf_style, + ) + and fonta + and fontb + and fonta.font_id == fontb.font_id + ) + # or len(composition.pdf_same_style_characters.pdf_character) == 1 + ): + chars.extend(composition.pdf_same_style_characters.pdf_character) + continue + placeholder = self.create_rich_text_placeholder( + composition.pdf_same_style_characters, placeholder_id, paragraph + ) + placeholders.append(placeholder) + # 样式需要一左一右两个占位符,所以 id+2 + placeholder_id = placeholder.id + 2 + chars.append(placeholder.left_placeholder) + chars.extend(composition.pdf_same_style_characters.pdf_character) + chars.append(placeholder.right_placeholder) + else: + logger.error( + "Unexpected PdfParagraphComposition type " + "in PdfParagraph during translation. " + f"Composition: {composition}. " + f"Paragraph: {paragraph}. " + ) + return None + + # 如果占位符数量超过50,且未禁用富文本翻译,则递归调用并禁用富文本翻译 + if len(placeholders) > 50 and not disable_rich_text_translate: + logger.warning( + f"Too many placeholders ({len(placeholders)}) in paragraph[{paragraph.debug_id}], " + "disabling rich text translation for this paragraph" + ) + return self.get_translate_input(paragraph, page_font_map, True) + + text = get_char_unicode_string(chars) + return self.TranslateInput(text, placeholders, paragraph.pdf_style) + + def process_formula( + self, formula: PdfFormula, formula_id: int, paragraph: PdfParagraph + ): + placeholder = self.create_formula_placeholder(formula, formula_id, paragraph) + if placeholder.placeholder in paragraph.unicode: + return self.process_formula(formula, formula_id + 1, paragraph) + + return placeholder + + def process_composition( + self, + composition: PdfSameStyleCharacters, + composition_id: int, + paragraph: PdfParagraph, + ): + placeholder = self.create_rich_text_placeholder( + composition, composition_id, paragraph + ) + if ( + placeholder.left_placeholder in paragraph.unicode + or placeholder.right_placeholder in paragraph.unicode + ): + return self.process_composition(composition, composition_id + 1, paragraph) + + return placeholder + + def parse_translate_output( + self, input_text: TranslateInput, output: str + ) -> [PdfParagraphComposition]: + import re + + result = [] + + # 如果没有占位符,直接返回整个文本 + if not input_text.placeholders: + comp = PdfParagraphComposition() + comp.pdf_same_style_unicode_characters = PdfSameStyleUnicodeCharacters() + comp.pdf_same_style_unicode_characters.unicode = output + comp.pdf_same_style_unicode_characters.pdf_style = input_text.base_style + return [comp] + + # 构建正则表达式模式 + patterns = [] + placeholder_patterns = [] + placeholder_map = {} + + for placeholder in input_text.placeholders: + if isinstance(placeholder, FormulaPlaceholder): + # 转义特殊字符 + pattern = re.escape(placeholder.placeholder) + patterns.append(f"({pattern})") + placeholder_patterns.append(f"({pattern})") + placeholder_map[placeholder.placeholder] = placeholder + else: + left = re.escape(placeholder.left_placeholder) + right = re.escape(placeholder.right_placeholder) + patterns.append(f"({left}.*?{right})") + placeholder_patterns.append(f"({left})") + placeholder_patterns.append(f"({right})") + placeholder_map[placeholder.left_placeholder] = placeholder + + # 合并所有模式 + combined_pattern = "|".join(patterns) + combined_placeholder_pattern = "|".join(placeholder_patterns) + + def remove_placeholder(text: str): + return re.sub(combined_placeholder_pattern, "", text) + + # 找到所有匹配 + last_end = 0 + for match in re.finditer(combined_pattern, output): + # 处理匹配之前的普通文本 + if match.start() > last_end: + text = output[last_end : match.start()] + if text: + comp = PdfParagraphComposition() + comp.pdf_same_style_unicode_characters = ( + PdfSameStyleUnicodeCharacters() + ) + comp.pdf_same_style_unicode_characters.unicode = remove_placeholder( + text + ) + comp.pdf_same_style_unicode_characters.pdf_style = ( + input_text.base_style + ) + result.append(comp) + + matched_text = match.group(0) + + # 处理占位符 + if any( + isinstance(p, FormulaPlaceholder) and matched_text == p.placeholder + for p in input_text.placeholders + ): + # 处理公式占位符 + placeholder = next( + p + for p in input_text.placeholders + if isinstance(p, FormulaPlaceholder) + and matched_text == p.placeholder + ) + comp = PdfParagraphComposition() + comp.pdf_formula = placeholder.formula + result.append(comp) + else: + # 处理富文本占位符 + placeholder = next( + p + for p in input_text.placeholders + if not isinstance(p, FormulaPlaceholder) + and matched_text.startswith(p.left_placeholder) + ) + text = matched_text[ + len(placeholder.left_placeholder) : -len( + placeholder.right_placeholder + ) + ] + + if isinstance( + placeholder.composition, PdfSameStyleCharacters + ) and text.replace(" ", "") == "".join( + x.char_unicode for x in placeholder.composition.pdf_character + ).replace(" ", ""): + comp = PdfParagraphComposition( + pdf_same_style_characters=placeholder.composition + ) + else: + comp = PdfParagraphComposition() + comp.pdf_same_style_unicode_characters = ( + PdfSameStyleUnicodeCharacters() + ) + comp.pdf_same_style_unicode_characters.pdf_style = ( + placeholder.composition.pdf_style + ) + comp.pdf_same_style_unicode_characters.unicode = remove_placeholder( + text + ) + result.append(comp) + + last_end = match.end() + + # 处理最后的普通文本 + if last_end < len(output): + text = output[last_end:] + if text: + comp = PdfParagraphComposition() + comp.pdf_same_style_unicode_characters = PdfSameStyleUnicodeCharacters() + comp.pdf_same_style_unicode_characters.unicode = remove_placeholder( + text + ) + comp.pdf_same_style_unicode_characters.pdf_style = input_text.base_style + result.append(comp) + + return result + + def translate_paragraph( + self, + paragraph: PdfParagraph, + pbar: tqdm | None = None, + tracker: ParagraphTranslateTracker = None, + page_font_map: dict[str, PdfFont] = None, + xobj_font_map: dict[int, dict[str, PdfFont]] = None, + ): + self.translation_config.raise_if_cancelled() + with PbarContext(pbar): + try: + if paragraph.vertical: + return + + tracker.set_pdf_unicode(paragraph.unicode) + if paragraph.xobj_id in xobj_font_map: + page_font_map = xobj_font_map[paragraph.xobj_id] + translate_input = self.get_translate_input(paragraph, page_font_map) + if not translate_input: + return + + tracker.set_input(translate_input.unicode) + + text = translate_input.unicode + + if len(text) < self.translation_config.min_text_length: + logger.debug( + f"Text too short to translate, skip. Text: {text}. Paragraph id: {paragraph.debug_id}." + ) + return + + translated_text = self.translate_engine.translate(text) + + tracker.set_output(translated_text) + + if translated_text == text: + return + + paragraph.unicode = translated_text + paragraph.pdf_paragraph_composition = self.parse_translate_output( + translate_input, translated_text + ) + for composition in paragraph.pdf_paragraph_composition: + if ( + composition.pdf_same_style_unicode_characters + and composition.pdf_same_style_unicode_characters.pdf_style + is None + ): + composition.pdf_same_style_unicode_characters.pdf_style = ( + paragraph.pdf_style + ) + except Exception as e: + logger.exception( + f"Error translating paragraph. Paragraph: {paragraph}. Error: {e}. " + ) + # ignore error and continue + return diff --git a/src/pdf2u/document_il/midend/layout_parser.py b/src/pdf2u/document_il/midend/layout_parser.py new file mode 100644 index 0000000000000000000000000000000000000000..ceb63ccd8b4abcfa79658d780d6b32e484abae1f --- /dev/null +++ b/src/pdf2u/document_il/midend/layout_parser.py @@ -0,0 +1,165 @@ +import logging +from pathlib import Path + +import cv2 +import numpy as np +from pymupdf import Document + +from pdf2u.document_il import il_version_1 +from pdf2u.document_il.utils.style_helper import GREEN +from pdf2u.translation_config import TranslationConfig + +logger = logging.getLogger(__name__) + + +class LayoutParser: + stage_name = "Parse Page Layout" + + def __init__(self, translation_config: TranslationConfig): + self.translation_config = translation_config + self.model = translation_config.doc_layout_model + + def _save_debug_image(self, image: np.ndarray, layout, page_number: int): + """Save debug image with drawn boxes if debug mode is enabled.""" + if not self.translation_config.debug: + return + + debug_dir = Path(self.translation_config.get_working_file_path("ocr-box-image")) + debug_dir.mkdir(parents=True, exist_ok=True) + + # Draw boxes on the image + debug_image = image.copy() + for box in layout.boxes: + x0, y0, x1, y1 = box.xyxy + cv2.rectangle( + debug_image, (int(x0), int(y0)), (int(x1), int(y1)), (0, 255, 0), 2 + ) + # Add text label + cv2.putText( + debug_image, + layout.names[box.cls], + (int(x0), int(y0) - 5), + cv2.FONT_HERSHEY_SIMPLEX, + 0.5, + (0, 255, 0), + 1, + ) + + # Save the image + output_path = debug_dir / f"{page_number}.jpg" + cv2.imwrite(str(output_path), debug_image) + + def _save_debug_box_to_page(self, page: il_version_1.Page): + """Save debug boxes and text labels to the PDF page.""" + if not self.translation_config.debug: + return + + color = GREEN + + for layout in page.page_layout: + # Create a rectangle box + rect = il_version_1.PdfRectangle( + box=il_version_1.Box( + x=layout.box.x, y=layout.box.y, x2=layout.box.x2, y2=layout.box.y2 + ), + graphic_state=color, + debug_info=True, + ) + page.pdf_rectangle.append(rect) + + # Create text label at top-left corner + # Note: PDF coordinates are from bottom-left, + # so we use y2 for top position + style = il_version_1.PdfStyle( + font_id="china-ss", font_size=4, graphic_state=color + ) + page.pdf_paragraph.append( + il_version_1.PdfParagraph( + first_line_indent=False, + box=il_version_1.Box( + x=layout.box.x, + y=layout.box.y2, + x2=layout.box.x2, + y2=layout.box.y2 + 5, + ), + vertical=False, + pdf_style=style, + unicode=layout.class_name, + pdf_paragraph_composition=[ + il_version_1.PdfParagraphComposition( + pdf_same_style_unicode_characters=il_version_1.PdfSameStyleUnicodeCharacters( + unicode=layout.class_name, + pdf_style=style, + debug_info=True, + ) + ) + ], + xobj_id=-1, + ) + ) + + def process(self, docs: il_version_1.Document, mupdf_doc: Document): + """Generate layouts for all pages that need to be translated.""" + # Get pages that need to be translated + pages_to_translate = [ + page + for page in docs.page + if self.translation_config.should_translate_page(page.page_number + 1) + ] + total = len(pages_to_translate) + with self.translation_config.progress_monitor.stage_start( + self.stage_name, total + ) as progress: + # Process pages in batches + batch_size = 16 + for i in range(0, total, batch_size): + self.translation_config.raise_if_cancelled() + batch_pages = pages_to_translate[i : i + batch_size] + + # Prepare batch images + batch_images = [] + for page in batch_pages: + pix = mupdf_doc[page.page_number].get_pixmap(dpi=72) + image = np.fromstring(pix.samples, np.uint8).reshape( + pix.height, pix.width, 3 + )[:, :, ::-1] + batch_images.append(image) + + # Get predictions for the batch + layouts_batch = self.model.predict(batch_images, batch_size=batch_size) + + # Process predictions for each page + for page, layouts in zip(batch_pages, layouts_batch, strict=False): + page_layouts = [] + self._save_debug_image( + batch_images[batch_pages.index(page)], + layouts, + page.page_number + 1, + ) + for layout in layouts.boxes: + # Convert coordinate system from picture to il + # system to the il coordinate system + x0, y0, x1, y1 = layout.xyxy + pix = mupdf_doc[page.page_number].get_pixmap() + h, w = pix.height, pix.width + x0, y0, x1, y1 = ( + np.clip(int(x0 - 1), 0, w - 1), + np.clip(int(h - y1 - 1), 0, h - 1), + np.clip(int(x1 + 1), 0, w - 1), + np.clip(int(h - y0 + 1), 0, h - 1), + ) + page_layout = il_version_1.PageLayout( + id=len(page_layouts) + 1, + box=il_version_1.Box( + x0.item(), y0.item(), x1.item(), y1.item() + ), + conf=layout.conf.item(), + class_name=layouts.names[layout.cls], + ) + page_layouts.append(page_layout) + + page.page_layout = page_layouts + self._save_debug_box_to_page(page) + progress.advance(1) + + return docs diff --git a/src/pdf2u/document_il/midend/paragraph_finder.py b/src/pdf2u/document_il/midend/paragraph_finder.py new file mode 100644 index 0000000000000000000000000000000000000000..fe64896eba861a696cfb6ff62a98fe7afce01a50 --- /dev/null +++ b/src/pdf2u/document_il/midend/paragraph_finder.py @@ -0,0 +1,446 @@ +import logging +import random +import re +from typing import Literal + +from pdf2u.document_il import ( + Box, + Page, + PdfCharacter, + PdfLine, + PdfParagraph, + PdfParagraphComposition, +) +from pdf2u.document_il.utils.layout_helper import ( + Layout, + add_space_dummy_chars, + get_char_unicode_string, +) +from pdf2u.translation_config import TranslationConfig + +logger = logging.getLogger(__name__) + +# Base58 alphabet (Bitcoin style, without numbers 0, O, I, l) +BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + + +def generate_base58_id(length: int = 5) -> str: + """Generate a random base58 ID of specified length.""" + return "".join(random.choice(BASE58_ALPHABET) for _ in range(length)) + + +class ParagraphFinder: + stage_name = "Parse Paragraphs" + + def __init__(self, translation_config: TranslationConfig): + self.translation_config = translation_config + + def update_paragraph_data(self, paragraph: PdfParagraph, update_unicode=False): + if not paragraph.pdf_paragraph_composition: + return + + chars = [] + for composition in paragraph.pdf_paragraph_composition: + if composition.pdf_line: + chars.extend(composition.pdf_line.pdf_character) + elif composition.pdf_formula: + chars.extend(composition.pdf_formula.pdf_character) + elif composition.pdf_same_style_unicode_characters: + continue + else: + logger.error( + "Unexpected composition type" + " in PdfParagraphComposition. " + "This type only appears in the IL " + "after the translation is completed." + ) + continue + + if update_unicode and chars: + paragraph.unicode = get_char_unicode_string(chars) + if not chars: + return + # 更新边界框 + min_x = min(char.box.x for char in chars) + min_y = min(char.box.y for char in chars) + max_x = max(char.box.x2 for char in chars) + max_y = max(char.box.y2 for char in chars) + paragraph.box = Box(min_x, min_y, max_x, max_y) + paragraph.vertical = chars[0].vertical + paragraph.xobj_id = chars[0].xobj_id + + paragraph.first_line_indent = False + if ( + paragraph.pdf_paragraph_composition[0].pdf_line + and paragraph.pdf_paragraph_composition[0].pdf_line.pdf_character[0].box.x + - paragraph.box.x + > 1 + ): + paragraph.first_line_indent = True + + def update_line_data(self, line: PdfLine): + min_x = min(char.box.x for char in line.pdf_character) + min_y = min(char.box.y for char in line.pdf_character) + max_x = max(char.box.x2 for char in line.pdf_character) + max_y = max(char.box.y2 for char in line.pdf_character) + line.box = Box(min_x, min_y, max_x, max_y) + + def process(self, document): + with self.translation_config.progress_monitor.stage_start( + self.stage_name, len(document.page) + ) as pbar: + for page in document.page: + self.translation_config.raise_if_cancelled() + self.process_page(page) + pbar.advance() + + def bbox_overlap(self, bbox1: Box, bbox2: Box) -> bool: + return ( + bbox1.x < bbox2.x2 + and bbox1.x2 > bbox2.x + and bbox1.y < bbox2.y2 + and bbox1.y2 > bbox2.y + ) + + def process_page(self, page: Page): + # 第一步:根据 layout 创建 paragraphs + # 在这一步中,page.pdf_character 中的字符会被移除 + paragraphs = self.create_paragraphs(page) + page.pdf_paragraph = paragraphs + + # 第二步:处理段落中的空格和换行符 + for paragraph in paragraphs: + add_space_dummy_chars(paragraph) + self.process_paragraph_spacing(paragraph) + self.update_paragraph_data(paragraph) + + # 第三步:计算所有行宽度的中位数 + median_width = self.calculate_median_line_width(paragraphs) + + # 第四步:处理独立段落 + self.process_independent_paragraphs(paragraphs, median_width) + + for paragraph in paragraphs: + self.update_paragraph_data(paragraph, update_unicode=True) + + def is_isolated_formula(self, char: PdfCharacter): + return char.char_unicode in ("(cid:122)", "(cid:123)", "(cid:124)", "(cid:125)") + + def create_paragraphs(self, page: Page) -> list[PdfParagraph]: + paragraphs: list[PdfParagraph] = [] + if page.pdf_paragraph: + paragraphs.extend(page.pdf_paragraph) + page.pdf_paragraph = [] + + current_paragraph: PdfParagraph | None = None + current_layout: Layout | None = None + current_line_chars: list[PdfCharacter] = [] + skip_chars = [] + + for char in page.pdf_character: + char_layout = self.get_layout(char, page) + if not self.is_text_layout(char_layout) or self.is_isolated_formula(char): + skip_chars.append(char) + continue + + # 检查是否需要开始新行 + if current_line_chars and Layout.is_newline(current_line_chars[-1], char): + # 创建新行 + if current_line_chars: + line = self.create_line(current_line_chars) + if current_paragraph is None: + current_paragraph = PdfParagraph( + pdf_paragraph_composition=[line], + debug_id=generate_base58_id(), + ) + paragraphs.append(current_paragraph) + else: + current_paragraph.pdf_paragraph_composition.append(line) + self.update_paragraph_data(current_paragraph) + current_line_chars = [] + + # 检查是否需要开始新段落 + if ( + current_layout is None + or char_layout.id != current_layout.id + or ( # 不是同一个 xobject + current_line_chars + and current_line_chars[-1].xobj_id != char.xobj_id + ) + ): + if current_line_chars: + line = self.create_line(current_line_chars) + if current_paragraph is not None: + current_paragraph.pdf_paragraph_composition.append(line) + self.update_paragraph_data(current_paragraph) + else: + current_paragraph = PdfParagraph( + pdf_paragraph_composition=[line], + debug_id=generate_base58_id(), + ) + self.update_paragraph_data(current_paragraph) + paragraphs.append(current_paragraph) + current_line_chars = [] + current_paragraph = None + current_layout = char_layout + + current_line_chars.append(char) + + # 处理最后一行的字符 + if current_line_chars: + line = self.create_line(current_line_chars) + if current_paragraph is None: + current_paragraph = PdfParagraph( + pdf_paragraph_composition=[line], debug_id=generate_base58_id() + ) + paragraphs.append(current_paragraph) + else: + current_paragraph.pdf_paragraph_composition.append(line) + self.update_paragraph_data(current_paragraph) + + page.pdf_character = skip_chars + + return paragraphs + + def process_paragraph_spacing(self, paragraph: PdfParagraph): + if not paragraph.pdf_paragraph_composition: + return + + # 处理行级别的空格 + processed_lines = [] + for composition in paragraph.pdf_paragraph_composition: + if not composition.pdf_line: + processed_lines.append(composition) + continue + + line = composition.pdf_line + if not "".join( + x.char_unicode for x in line.pdf_character + ).strip(): # 跳过完全空白的行 + continue + + # 处理行内字符的尾随空格 + processed_chars = [] + for char in line.pdf_character: + if not char.char_unicode.isspace(): + processed_chars = processed_chars + [char] + elif processed_chars: # 只有在有非空格字符后才考虑保留空格 + processed_chars.append(char) + + # 移除尾随空格 + while processed_chars and processed_chars[-1].char_unicode.isspace(): + processed_chars.pop() + + if processed_chars: # 如果行内还有字符 + line = self.create_line(processed_chars) + processed_lines.append(line) + + paragraph.pdf_paragraph_composition = processed_lines + self.update_paragraph_data(paragraph) + + def is_text_layout(self, layout: Layout): + return layout is not None and layout.name in [ + "plain text", + "title", + "abandon", + "figure_caption", + "table_caption", + ] + + def get_layout( + self, + char: PdfCharacter, + page: Page, + xy_mode: Literal["topleft"] + | Literal["bottomright"] + | Literal["middle"] = "middle", + ): + tl, br, md = [ + self._get_layout(char, page, mode) + for mode in ["topleft", "bottomright", "middle"] + ] + if tl is not None and tl.name == "isolate_formula": + return tl + if br is not None and br.name == "isolate_formula": + return br + if md is not None and md.name == "isolate_formula": + return md + + if md is not None: + return md + if tl is not None: + return tl + return br + + def _get_layout( + self, + char: PdfCharacter, + page: Page, + xy_mode: Literal["topleft"] + | Literal["bottomright"] + | Literal["middle"] = "middle", + ): + # 这几个符号,解析出来的大小经常只有实际大小的一点点。 + # if ( + # xy_mode != "bottomright" + # and char.char_unicode in HEIGHT_NOT_USFUL_CHAR_IN_CHAR + # ): + # return self.get_layout(char, page, "bottomright") + # current layouts + # { + # "title", + # "plain text", + # "abandon", + # "figure", + # "figure_caption", + # "table", + # "table_caption", + # "table_footnote", + # "isolate_formula", + # "formula_caption", + # } + layout_priority = [ + "formula_caption", + "isolate_formula", + "table_footnote", + "table", + "figure", + "table_caption", + "figure_caption", + "abandon", + "plain text", + "title", + ] + char_box = char.box + if xy_mode == "topleft": + char_x = char_box.x + char_y = char_box.y2 + elif xy_mode == "bottomright": + char_x = char_box.x2 + char_y = char_box.y + elif xy_mode == "middle": + char_x = (char_box.x + char_box.x2) / 2 + char_y = (char_box.y + char_box.y2) / 2 + else: + logger.error(f"Invalid xy_mode: {xy_mode}") + return self.get_layout(char, page, "middle") + # 按照优先级顺序检查每种布局 + matching_layouts = {} + for layout in page.page_layout: + layout_box = layout.box + if ( + layout_box.x <= char_x <= layout_box.x2 + and layout_box.y <= char_y <= layout_box.y2 + ): + matching_layouts[layout.class_name] = Layout( + layout.id, layout.class_name + ) + + # 按照优先级返回最高优先级的布局 + for layout_name in layout_priority: + if layout_name in matching_layouts: + return matching_layouts[layout_name] + + return None + + def create_line(self, chars: list[PdfCharacter]) -> PdfParagraphComposition: + assert chars + + line = PdfLine(pdf_character=chars) + self.update_line_data(line) + return PdfParagraphComposition(pdf_line=line) + + def calculate_median_line_width(self, paragraphs: list[PdfParagraph]) -> float: + # 收集所有行的宽度 + line_widths = [] + for paragraph in paragraphs: + for composition in paragraph.pdf_paragraph_composition: + if composition.pdf_line: + line = composition.pdf_line + line_widths.append(line.box.x2 - line.box.x) + + if not line_widths: + return 0.0 + + # 计算中位数 + line_widths.sort() + mid = len(line_widths) // 2 + if len(line_widths) % 2 == 0: + return (line_widths[mid - 1] + line_widths[mid]) / 2 + return line_widths[mid] + + def process_independent_paragraphs( + self, paragraphs: list[PdfParagraph], median_width: float + ): + i = 0 + while i < len(paragraphs): + paragraph = paragraphs[i] + if len(paragraph.pdf_paragraph_composition) <= 1: # 跳过只有一行的段落 + i += 1 + continue + + j = 1 + while j < len(paragraph.pdf_paragraph_composition): + prev_composition = paragraph.pdf_paragraph_composition[j - 1] + if not prev_composition.pdf_line: + j += 1 + continue + + prev_line = prev_composition.pdf_line + prev_width = prev_line.box.x2 - prev_line.box.x + prev_text = "".join([c.char_unicode for c in prev_line.pdf_character]) + + # 检查是否包含连续的点(至少 20 个) + # 如果有至少连续 20 个点,则代表这是目录条目 + if re.search(r"\.{20,}", prev_text): + # 创建新的段落 + new_paragraph = PdfParagraph( + box=Box(0, 0, 0, 0), # 临时边界框 + pdf_paragraph_composition=( + paragraph.pdf_paragraph_composition[j:] + ), + unicode="", + debug_id=generate_base58_id(), + ) + # 更新原段落 + paragraph.pdf_paragraph_composition = ( + paragraph.pdf_paragraph_composition[:j] + ) + + # 更新两个段落的数据 + self.update_paragraph_data(paragraph) + self.update_paragraph_data(new_paragraph) + + # 在原段落后插入新段落 + paragraphs.insert(i + 1, new_paragraph) + break + + # 如果前一行宽度小于中位数的一半,将当前行及后续行分割成新段落 + if ( + self.translation_config.split_short_lines + and prev_width + < median_width * self.translation_config.short_line_split_factor + ): + # 创建新的段落 + new_paragraph = PdfParagraph( + box=Box(0, 0, 0, 0), # 临时边界框 + pdf_paragraph_composition=( + paragraph.pdf_paragraph_composition[j:] + ), + unicode="", + debug_id=generate_base58_id(), + ) + # 更新原段落 + paragraph.pdf_paragraph_composition = ( + paragraph.pdf_paragraph_composition[:j] + ) + + # 更新两个段落的数据 + self.update_paragraph_data(paragraph) + self.update_paragraph_data(new_paragraph) + + # 在原段落后插入新段落 + paragraphs.insert(i + 1, new_paragraph) + break + j += 1 + i += 1 diff --git a/src/pdf2u/document_il/midend/remove_descent.py b/src/pdf2u/document_il/midend/remove_descent.py new file mode 100644 index 0000000000000000000000000000000000000000..ec437ac435442490bc211b29f2c981a49138a03d --- /dev/null +++ b/src/pdf2u/document_il/midend/remove_descent.py @@ -0,0 +1,162 @@ +import logging +from collections import Counter +from functools import cache + +from pdf2u.document_il import il_version_1 +from pdf2u.translation_config import TranslationConfig + +logger = logging.getLogger(__name__) + + +class RemoveDescent: + stage_name = "Remove Char Descent" + + def __init__(self, translation_config: TranslationConfig): + self.translation_config = translation_config + + def _remove_char_descent( + self, char: il_version_1.PdfCharacter, font: il_version_1.PdfFont + ) -> float | None: + """Remove descent from a single character and return the descent value. + + Args: + char: The character to process + font: The font used by this character + + Returns: + The descent value if it was removed, None otherwise + """ + if ( + char.box + and char.box.y is not None + and char.box.y2 is not None + and font + and hasattr(font, "descent") + ): + descent = font.descent * char.pdf_style.font_size / 1000 + if char.vertical: + # For vertical text, remove descent from x coordinates + char.box.x += descent + char.box.x2 += descent + else: + # For horizontal text, remove descent from y coordinates + char.box.y -= descent + char.box.y2 -= descent + return descent + return None + + def process(self, document: il_version_1.Document): + """Process the document to remove descent adjustments from character boxes. + + Args: + document: The document to process + """ + with self.translation_config.progress_monitor.stage_start( + self.stage_name, len(document.page) + ) as pbar: + for page in document.page: + self.translation_config.raise_if_cancelled() + self.process_page(page) + pbar.advance() + + def process_page(self, page: il_version_1.Page): + """Process a single page to remove descent adjustments. + + Args: + page: The page to process + """ + # Build font map including xobjects + fonts: dict[ + str | int, il_version_1.PdfFont | dict[str, il_version_1.PdfFont] + ] = {f.font_id: f for f in page.pdf_font} + page_fonts = {f.font_id: f for f in page.pdf_font} + + # Add xobject fonts + for xobj in page.pdf_xobject: + fonts[xobj.xobj_id] = page_fonts.copy() + for font in xobj.pdf_font: + fonts[xobj.xobj_id][font.font_id] = font + + @cache + def get_font( + font_id: str, xobj_id: int | None = None + ) -> il_version_1.PdfFont | None: + if xobj_id is not None and xobj_id in fonts: + font_map = fonts[xobj_id] + if isinstance(font_map, dict) and font_id in font_map: + return font_map[font_id] + return ( + fonts.get(font_id) + if isinstance(fonts.get(font_id), il_version_1.PdfFont) + else None + ) + + # Process all standalone characters in the page + for char in page.pdf_character: + if font := get_font(char.pdf_style.font_id, char.xobj_id): + self._remove_char_descent(char, font) + + # Process all paragraphs + for paragraph in page.pdf_paragraph: + descent_values = [] + vertical_chars = [] + + # Process all characters in paragraph compositions + for comp in paragraph.pdf_paragraph_composition: + # Handle direct characters + if comp.pdf_character: + font = get_font( + comp.pdf_character.pdf_style.font_id, comp.pdf_character.xobj_id + ) + if font: + descent = self._remove_char_descent(comp.pdf_character, font) + if descent is not None: + descent_values.append(descent) + vertical_chars.append(comp.pdf_character.vertical) + + # Handle characters in PdfLine + elif comp.pdf_line: + for char in comp.pdf_line.pdf_character: + if font := get_font(char.pdf_style.font_id, char.xobj_id): + descent = self._remove_char_descent(char, font) + if descent is not None: + descent_values.append(descent) + vertical_chars.append(char.vertical) + + # Handle characters in PdfFormula + elif comp.pdf_formula: + for char in comp.pdf_formula.pdf_character: + if font := get_font(char.pdf_style.font_id, char.xobj_id): + descent = self._remove_char_descent(char, font) + if descent is not None: + descent_values.append(descent) + vertical_chars.append(char.vertical) + + # Handle characters in PdfSameStyleCharacters + elif comp.pdf_same_style_characters: + for char in comp.pdf_same_style_characters.pdf_character: + if font := get_font(char.pdf_style.font_id, char.xobj_id): + descent = self._remove_char_descent(char, font) + if descent is not None: + descent_values.append(descent) + vertical_chars.append(char.vertical) + + # Adjust paragraph box based on most common descent value + if descent_values and paragraph.box: + # Calculate mode of descent values + descent_counter = Counter(descent_values) + most_common_descent = descent_counter.most_common(1)[0][0] + + # Check if paragraph is vertical (all characters are vertical) + is_vertical = all(vertical_chars) if vertical_chars else False + + # Adjust paragraph box + if paragraph.box.y is not None and paragraph.box.y2 is not None: + if is_vertical: + # For vertical paragraphs, adjust x coordinates + paragraph.box.x += most_common_descent + paragraph.box.x2 += most_common_descent + else: + # For horizontal paragraphs, adjust y coordinates + paragraph.box.y -= most_common_descent + paragraph.box.y2 -= most_common_descent diff --git a/src/pdf2u/document_il/midend/styles_and_formulas.py b/src/pdf2u/document_il/midend/styles_and_formulas.py new file mode 100644 index 0000000000000000000000000000000000000000..360c4c8c7d4f5bad56da038d68160870431337d7 --- /dev/null +++ b/src/pdf2u/document_il/midend/styles_and_formulas.py @@ -0,0 +1,692 @@ +import base64 +import math +import re +import unicodedata + +from pdf2u.document_il.il_version_1 import ( + Box, + Document, + GraphicState, + Page, + PdfCharacter, + PdfFormula, + PdfLine, + PdfParagraphComposition, + PdfSameStyleCharacters, + PdfStyle, +) +from pdf2u.document_il.utils.fontmap import FontMapper +from pdf2u.document_il.utils.layout_helper import ( + LEFT_BRACKET, + RIGHT_BRACKET, + formular_height_ignore_char, + is_same_style, +) +from pdf2u.translation_config import TranslationConfig + + +class StylesAndFormulas: + stage_name = "Parse Formulas and Styles" + + def __init__(self, translation_config: TranslationConfig): + self.translation_config = translation_config + self.font_mapper = FontMapper(translation_config) + + def process(self, document: Document): + with self.translation_config.progress_monitor.stage_start( + self.stage_name, len(document.page) + ) as pbar: + for page in document.page: + self.translation_config.raise_if_cancelled() + self.process_page(page) + pbar.advance() + + def process_page(self, page: Page): + """处理页面,包括公式识别和偏移量计算""" + self.process_page_formulas(page) + self.process_page_offsets(page) + self.process_comma_formulas(page) + self.merge_overlapping_formulas(page) + self.process_page_offsets(page) + self.process_translatable_formulas(page) + self.process_page_styles(page) + + def update_line_data(self, line: PdfLine): + min_x = min(char.box.x for char in line.pdf_character) + min_y = min(char.box.y for char in line.pdf_character) + max_x = max(char.box.x2 for char in line.pdf_character) + max_y = max(char.box.y2 for char in line.pdf_character) + line.box = Box(min_x, min_y, max_x, max_y) + + def process_page_formulas(self, page: Page): + if not page.pdf_paragraph: + return + + # 收集该页所有的公式字体ID + formula_font_ids = set() + for font in page.pdf_font: + if self.is_formulas_font(font.name): + formula_font_ids.add(font.font_id) + + for paragraph in page.pdf_paragraph: + if not paragraph.pdf_paragraph_composition: + continue + + new_compositions = [] + + for composition in paragraph.pdf_paragraph_composition: + current_chars = [] + in_formula_state = False # 当前是否在处理公式字符 + in_corner_mark_state = False + + line = composition.pdf_line + if not line: + new_compositions.append(composition) + continue + for char in line.pdf_character: + is_formula = ( + ( # 区分公式开头的字符&公式中间的字符。主要是逗号不能在公式开头,但是可以在中间。 + ( + self.is_formulas_start_char(char.char_unicode) + and not in_formula_state + ) + or ( + self.is_formulas_middle_char(char.char_unicode) + and in_formula_state + ) + ) # 公式字符 + or char.pdf_style.font_id in formula_font_ids # 公式字体 + or char.vertical # 垂直字体 + or ( + # 如果是程序添加的dummy空格 + char.char_unicode is None and in_formula_state + ) + ) + + # isspace = get_char_unicode_string(current_chars).isspace() + isspace = all(x.char_unicode.isspace() for x in current_chars) + is_corner_mark = ( + len(current_chars) > 0 + and not isspace + # 角标字体,有 0.76 的角标和 0.799 的大写,这里用 0.79 取中,同时考虑首字母放大的情况 + and char.pdf_style.font_size + < current_chars[-1].pdf_style.font_size * 0.79 + and not in_corner_mark_state + ) or ( + len(current_chars) > 0 + and not isspace + # 角标字体,有 0.76 的角标和 0.799 的大写,这里用 0.79 取中,同时考虑首字母放大的情况 + and char.pdf_style.font_size + < current_chars[-1].pdf_style.font_size * 1.1 + and in_corner_mark_state + ) + + is_formula = is_formula or is_corner_mark + + if char.char_unicode == " ": + is_formula = in_formula_state + + if is_formula != in_formula_state and current_chars: + # 字符类型发生切换,处理之前的字符 + new_compositions.append( + self.create_composition(current_chars, in_formula_state) + ) + current_chars = [] + in_formula_state = is_formula + in_corner_mark_state = is_corner_mark + + current_chars.append(char) + + # 处理行末的字符 + if current_chars: + new_compositions.append( + self.create_composition(current_chars, in_formula_state) + ) + current_chars = [] + + paragraph.pdf_paragraph_composition = new_compositions + + def process_translatable_formulas(self, page: Page): + """将需要正常翻译的公式(如纯数字、数字加逗号等)转换为普通文本行""" + if not page.pdf_paragraph: + return + + for paragraph in page.pdf_paragraph: + if not paragraph.pdf_paragraph_composition: + continue + + new_compositions = [] + for composition in paragraph.pdf_paragraph_composition: + if composition.pdf_formula is not None and self.is_translatable_formula( + composition.pdf_formula + ): + # 将可翻译公式转换为普通文本行 + new_line = PdfLine( + pdf_character=composition.pdf_formula.pdf_character + ) + self.update_line_data(new_line) + new_compositions.append(PdfParagraphComposition(pdf_line=new_line)) + else: + new_compositions.append(composition) + + paragraph.pdf_paragraph_composition = new_compositions + + def process_page_styles(self, page: Page): + """处理页面中的文本样式,识别相同样式的文本""" + if not page.pdf_paragraph: + return + + for paragraph in page.pdf_paragraph: + if not paragraph.pdf_paragraph_composition: + continue + + # 计算基准样式(除公式外所有文字样式的交集) + base_style = self._calculate_base_style(paragraph) + paragraph.pdf_style = base_style + + # 重新组织段落中的文本,将相同样式的文本组合在一起 + new_compositions = [] + current_chars = [] + current_style = None + + for comp in paragraph.pdf_paragraph_composition: + if comp.pdf_formula is not None: + if current_chars: + new_comp = self._create_same_style_composition( + current_chars, current_style + ) + new_compositions.append(new_comp) + current_chars = [] + new_compositions.append(comp) + continue + + if not comp.pdf_line: + new_compositions.append(comp) + continue + + for char in comp.pdf_line.pdf_character: + char_style = char.pdf_style + if current_style is None: + current_style = char_style + current_chars.append(char) + elif is_same_style(char_style, current_style): + current_chars.append(char) + else: + if current_chars: + new_comp = self._create_same_style_composition( + current_chars, current_style + ) + new_compositions.append(new_comp) + current_chars = [char] + current_style = char_style + + if current_chars: + new_comp = self._create_same_style_composition( + current_chars, current_style + ) + new_compositions.append(new_comp) + + paragraph.pdf_paragraph_composition = new_compositions + + def _calculate_base_style(self, paragraph) -> PdfStyle: + """计算段落的基准样式(除公式外所有文字样式的交集)""" + styles = [] + for comp in paragraph.pdf_paragraph_composition: + if isinstance(comp, PdfFormula): + continue + if not comp.pdf_line: + continue + for char in comp.pdf_line.pdf_character: + styles.append(char.pdf_style) + + if not styles: + return None + + # 返回所有样式的交集 + base_style = styles[0] + for style in styles[1:]: + # 更新基准样式为所有样式的交集 + base_style = self._merge_styles(base_style, style) + + # 如果font_id或font_size为None,则使用众数 + if base_style.font_id is None: + base_style.font_id = self._get_mode_value([s.font_id for s in styles]) + if base_style.font_size is None: + base_style.font_size = self._get_mode_value([s.font_size for s in styles]) + + return base_style + + def _get_mode_value(self, values): + """计算列表中的众数""" + if not values: + return None + from collections import Counter + + counter = Counter(values) + return counter.most_common(1)[0][0] + + def _merge_styles(self, style1, style2): + """合并两个样式,返回它们的交集""" + if style1 is None or style1.font_size is None: + return style2 + if style2 is None or style2.font_size is None: + return style1 + + return PdfStyle( + font_id=style1.font_id if style1.font_id == style2.font_id else None, + font_size=( + style1.font_size + if math.fabs(style1.font_size - style2.font_size) < 0.02 + else None + ), + graphic_state=self._merge_graphic_states( + style1.graphic_state, style2.graphic_state + ), + ) + + def _merge_graphic_states(self, state1, state2): + """合并两个GraphicState,返回它们的交集""" + if state1 is None: + return state2 + if state2 is None: + return state1 + + return GraphicState( + linewidth=( + state1.linewidth if state1.linewidth == state2.linewidth else None + ), + dash=state1.dash if state1.dash == state2.dash else None, + flatness=state1.flatness if state1.flatness == state2.flatness else None, + intent=state1.intent if state1.intent == state2.intent else None, + linecap=state1.linecap if state1.linecap == state2.linecap else None, + linejoin=state1.linejoin if state1.linejoin == state2.linejoin else None, + miterlimit=( + state1.miterlimit if state1.miterlimit == state2.miterlimit else None + ), + ncolor=state1.ncolor if state1.ncolor == state2.ncolor else None, + scolor=state1.scolor if state1.scolor == state2.scolor else None, + stroking_color_space_name=( + state1.stroking_color_space_name + if state1.stroking_color_space_name == state2.stroking_color_space_name + else None + ), + non_stroking_color_space_name=( + state1.non_stroking_color_space_name + if state1.non_stroking_color_space_name + == state2.non_stroking_color_space_name + else None + ), + passthrough_per_char_instruction=( + state1.passthrough_per_char_instruction + if state1.passthrough_per_char_instruction + == state2.passthrough_per_char_instruction + else None + ), + ) + + def _create_same_style_composition( + self, chars: list[PdfCharacter], style + ) -> PdfParagraphComposition: + """创建具有相同样式的文本组合""" + if not chars: + return None + + # 计算边界框 + min_x = min(char.box.x for char in chars) + min_y = min(char.box.y for char in chars) + max_x = max(char.box.x2 for char in chars) + max_y = max(char.box.y2 for char in chars) + box = Box(min_x, min_y, max_x, max_y) + + return PdfParagraphComposition( + pdf_same_style_characters=PdfSameStyleCharacters( + box=box, pdf_style=style, pdf_character=chars + ) + ) + + def process_page_offsets(self, page: Page): + """计算公式的x和y偏移量""" + if not page.pdf_paragraph: + return + + for paragraph in page.pdf_paragraph: + if not paragraph.pdf_paragraph_composition: + continue + + # 计算该段落的行间距,用其80%作为容差 + line_spacing = self.calculate_line_spacing(paragraph) + y_tolerance = line_spacing * 0.8 + + for i, composition in enumerate(paragraph.pdf_paragraph_composition): + if not composition.pdf_formula: + continue + + formula = composition.pdf_formula + left_line = None + right_line = None + + # 查找左边最近的同一行的文本 + for j in range(i - 1, -1, -1): + comp = paragraph.pdf_paragraph_composition[j] + if comp.pdf_line: + # 检查y坐标是否接近,判断是否在同一行 + if abs(comp.pdf_line.box.y - formula.box.y) <= y_tolerance: + left_line = comp.pdf_line + break + + # 查找右边最近的同一行的文本 + for j in range(i + 1, len(paragraph.pdf_paragraph_composition)): + comp = paragraph.pdf_paragraph_composition[j] + if comp.pdf_line: + # 检查y坐标是否接近,判断是否在同一行 + if abs(comp.pdf_line.box.y - formula.box.y) <= y_tolerance: + right_line = comp.pdf_line + break + + # 计算x偏移量(相对于左边文本) + if left_line: + formula.x_offset = formula.box.x - left_line.box.x2 + else: + formula.x_offset = 0 # 如果左边没有文字,x_offset应该为0 + if abs(formula.x_offset) < 0.1: + formula.x_offset = 0 + if formula.x_offset > 0: + formula.x_offset = 0 + + # 计算y偏移量 + if left_line: + # 使用底部坐标计算偏移量 + formula.y_offset = formula.box.y - left_line.box.y + elif right_line: + formula.y_offset = formula.box.y - right_line.box.y + else: + formula.y_offset = 0 + + if abs(formula.y_offset) < 0.1: + formula.y_offset = 0 + + def calculate_line_spacing(self, paragraph) -> float: + """计算段落中的平均行间距""" + if not paragraph.pdf_paragraph_composition: + return 0.0 + + # 收集所有文本行的y坐标 + line_y_positions = [] + for comp in paragraph.pdf_paragraph_composition: + if comp.pdf_line: + line_y_positions.append(comp.pdf_line.box.y) + + if len(line_y_positions) < 2: + return 10.0 # 如果只有一行或没有行,返回一个默认值 + + # 计算相邻行之间的y差值 + line_spacings = [] + for i in range(len(line_y_positions) - 1): + spacing = abs(line_y_positions[i] - line_y_positions[i + 1]) + if spacing > 0: # 忽略重叠的行 + line_spacings.append(spacing) + + if not line_spacings: + return 10.0 # 如果没有有效的行间距,返回默认值 + + # 使用中位数来避免异常值的影响 + median_spacing = sorted(line_spacings)[len(line_spacings) // 2] + return median_spacing + + def create_composition( + self, chars: list[PdfCharacter], is_formula: bool + ) -> PdfParagraphComposition: + if is_formula: + formula = PdfFormula(pdf_character=chars) + self.update_formula_data(formula) + return PdfParagraphComposition(pdf_formula=formula) + else: + new_line = PdfLine(pdf_character=chars) + self.update_line_data(new_line) + return PdfParagraphComposition(pdf_line=new_line) + + def update_formula_data(self, formula: PdfFormula): + min_x = min(char.box.x for char in formula.pdf_character) + max_x = max(char.box.x2 for char in formula.pdf_character) + if not all(map(formular_height_ignore_char, formula.pdf_character)): + min_y = min( + char.box.y + for char in formula.pdf_character + if not formular_height_ignore_char(char) + ) + max_y = max( + char.box.y2 + for char in formula.pdf_character + if not formular_height_ignore_char(char) + ) + else: + min_y = min(char.box.y for char in formula.pdf_character) + max_y = max(char.box.y2 for char in formula.pdf_character) + formula.box = Box(min_x, min_y, max_x, max_y) + + def is_translatable_formula(self, formula: PdfFormula) -> bool: + """判断公式是否只包含需要正常翻译的字符(数字、空格和英文逗号)""" + text = "".join(char.char_unicode for char in formula.pdf_character) + if formula.y_offset > 0.1: + return False + return bool(re.match(r"^[0-9, ]+$", text)) + + def is_formulas_font(self, font_name: str) -> bool: + pattern2 = r"^(Cambria|Cambria-BoldItalic|Cambria-Bold|Cambria-Italic)$" + if self.translation_config.formular_font_pattern: + pattern = self.translation_config.formular_font_pattern + else: + pattern = ( + r"(CM[^RB]" + r"|(MS|XY|MT|BL|RM|EU|LA|RS)[A-Z]" + r"|LINE" + r"|LCIRCLE" + r"|TeX-" + r"|rsfs" + r"|txsy" + r"|wasy" + r"|stmary" + r"|.*Mono" + r"|.*Code" + r"|.*Ital" + r"|.*Sym" + r"|.*Math" + r")" + ) + + if font_name.startswith("BASE64:"): + font_name_bytes = base64.b64decode(font_name[7:]) + font = font_name_bytes.split(b"+")[-1] + pattern2 = pattern2.encode() + pattern = pattern.encode() + else: + font = font_name.split("+")[-1] + + if re.match(pattern2, font): + return False + if re.match(pattern, font): + return True + + return False + + def is_formulas_start_char(self, char: str) -> bool: + if "(cid:" in char: + return True + if not self.font_mapper.has_char(char): + return True + if self.translation_config.formular_char_pattern: + pattern = self.translation_config.formular_char_pattern + if re.match(pattern, char): + return True + if ( + char + and char != " " # 非空格 + and ( + unicodedata.category(char[0]) + in [ + "Lm", + "Mn", + "Sk", + "Sm", + "Zl", + "Zp", + "Zs", + "Co", # private use character + "So", # symbol + ] # 文字修饰符、数学符号、分隔符号 + or ord(char[0]) in range(0x370, 0x400) # 希腊字母 + ) + ): + return True + if re.match("[0-9\\[\\]•]", char): + return True + return False + + def is_formulas_middle_char(self, char: str) -> bool: + if self.is_formulas_start_char(char): + return True + + if re.match(",", char): + return True + + def should_split_formula(self, formula: PdfFormula) -> bool: + """判断公式是否需要按逗号拆分(包含逗号且有其他特殊符号)""" + text = "".join(char.char_unicode for char in formula.pdf_character) + # 必须包含逗号 + if "," not in text: + return False + # 检查是否包含除了数字和[]之外的其他符号 + text_without_basic = re.sub(r"[0-9\[\],\s]", "", text) + return bool(text_without_basic) + + def split_formula_by_comma( + self, formula: PdfFormula + ) -> list[tuple[list[PdfCharacter], PdfCharacter]]: + """按逗号拆分公式字符,返回(字符组, 逗号字符)的列表,最后一组的逗号字符为None。 + 只有不在括号内的逗号才会被用作分隔符。支持的括号对包括: + - (cid:8) 和 (cid:9) + - ( 和 ) + - (cid:16) 和 (cid:17) + """ + result = [] + current_chars = [] + bracket_level = 0 # 跟踪括号的层数 + + for char in formula.pdf_character: + # 检查是否是左括号 + if char.char_unicode in LEFT_BRACKET: + bracket_level += 1 + current_chars.append(char) + # 检查是否是右括号 + elif char.char_unicode in RIGHT_BRACKET: + bracket_level = max(0, bracket_level - 1) # 防止括号不匹配的情况 + current_chars.append(char) + # 检查是否是逗号,且不在括号内 + elif char.char_unicode == "," and bracket_level == 0: + if current_chars: + result.append((current_chars, char)) + current_chars = [] + else: + current_chars.append(char) + + if current_chars: + result.append((current_chars, None)) # 最后一组没有逗号 + + return result + + def merge_formulas(self, formula1: PdfFormula, formula2: PdfFormula) -> PdfFormula: + """合并两个公式,保持字符的相对位置""" + # 合并所有字符 + all_chars = formula1.pdf_character + formula2.pdf_character + # 按y坐标和x坐标排序,确保字符顺序正确 + sorted_chars = sorted(all_chars, key=lambda c: (c.box.y, c.box.x)) + + merged_formula = PdfFormula(pdf_character=sorted_chars) + self.update_formula_data(merged_formula) + return merged_formula + + def merge_overlapping_formulas(self, page: Page): + """ + 合并x轴重叠且y轴有交集的相邻公式 + 角标可能会被识别成单独的公式,需要合并 + """ + if not page.pdf_paragraph: + return + + for paragraph in page.pdf_paragraph: + if not paragraph.pdf_paragraph_composition: + continue + + i = 0 + while i < len(paragraph.pdf_paragraph_composition) - 1: + comp1 = paragraph.pdf_paragraph_composition[i] + comp2 = paragraph.pdf_paragraph_composition[i + 1] + + # 检查是否都是公式 + if comp1.pdf_formula is None or comp2.pdf_formula is None: + i += 1 + continue + + formula1 = comp1.pdf_formula + formula2 = comp2.pdf_formula + + # 检查x轴重叠和y轴交集 + if self.is_x_axis_contained( + formula1.box, formula2.box + ) and self.has_y_intersection(formula1.box, formula2.box): + # 合并公式 + merged_formula = self.merge_formulas(formula1, formula2) + paragraph.pdf_paragraph_composition[i] = PdfParagraphComposition( + pdf_formula=merged_formula + ) + # 删除第二个公式 + del paragraph.pdf_paragraph_composition[i + 1] + # 不增加i,因为合并后的公式可能还需要和下一个公式合并 + else: + i += 1 + + def is_x_axis_contained(self, box1: Box, box2: Box) -> bool: + """判断box1的x轴是否完全包含在box2的x轴内,或反之""" + return (box1.x >= box2.x and box1.x2 <= box2.x2) or ( + box2.x >= box1.x and box2.x2 <= box1.x2 + ) + + def has_y_intersection(self, box1: Box, box2: Box) -> bool: + """判断两个box的y轴是否有交集""" + return not (box1.y2 < box2.y or box2.y2 < box1.y) + + def process_comma_formulas(self, page: Page): + """处理包含逗号的复杂公式,将其按逗号拆分""" + if not page.pdf_paragraph: + return + + for paragraph in page.pdf_paragraph: + if not paragraph.pdf_paragraph_composition: + continue + + new_compositions = [] + for composition in paragraph.pdf_paragraph_composition: + if composition.pdf_formula is not None and self.should_split_formula( + composition.pdf_formula + ): + # 按逗号拆分公式 + char_groups = self.split_formula_by_comma(composition.pdf_formula) + for chars, comma in char_groups: + if chars: # 忽略空组(连续的逗号) + formula = PdfFormula(pdf_character=chars) + self.update_formula_data(formula) + new_compositions.append( + PdfParagraphComposition(pdf_formula=formula) + ) + + # 如果有逗号,添加为文本行 + if comma: + comma_line = PdfLine(pdf_character=[comma]) + self.update_line_data(comma_line) + new_compositions.append( + PdfParagraphComposition(pdf_line=comma_line) + ) + else: + new_compositions.append(composition) + + paragraph.pdf_paragraph_composition = new_compositions diff --git a/src/pdf2u/document_il/midend/typesetting.py b/src/pdf2u/document_il/midend/typesetting.py new file mode 100644 index 0000000000000000000000000000000000000000..599ab4c013694461999f397b313e855a822b6d5c --- /dev/null +++ b/src/pdf2u/document_il/midend/typesetting.py @@ -0,0 +1,727 @@ +import logging +import statistics +import unicodedata +from functools import cache + +import pymupdf + +from pdf2u.document_il import ( + Box, + PdfCharacter, + PdfFormula, + PdfParagraphComposition, + PdfStyle, + il_version_1, +) +from pdf2u.document_il.utils.fontmap import FontMapper +from pdf2u.translation_config import TranslationConfig + +logger = logging.getLogger(__name__) + + +class TypesettingUnit: + def __str__(self): + return self.try_get_unicode() + + def __init__( + self, + char: PdfCharacter = None, + formular: PdfFormula = None, + unicode: str = None, + font: pymupdf.Font | None = None, + font_size: float = None, + style: PdfStyle = None, + xobj_id: int = None, + debug_info: bool = False, + ): + assert sum(x is not None for x in [char, formular, unicode]) == 1, ( + "Only one of chars and formular can be not None" + ) + self.char = char + self.formular = formular + self.unicode = unicode + self.x = None + self.y = None + self.scale = None + self.debug_info = debug_info + + if unicode: + assert font_size, "Font size must be provided when unicode is provided" + assert style, "Style must be provided when unicode is provided" + assert len(unicode) == 1, "Unicode must be a single character" + assert xobj_id is not None, ( + "Xobj id must be provided when unicode is provided" + ) + + self.font = font + self.font_id = font.font_id + self.font_size = font_size + self.style = style + self.xobj_id = xobj_id + + def try_get_unicode(self) -> str | None: + if self.char: + return self.char.char_unicode + elif self.formular: + return None + elif self.unicode: + return self.unicode + + @property + def mixed_character_blacklist(self): + unicode = self.try_get_unicode() + if unicode: + return unicode in ["。", ",", ":", "?", "!"] + return False + + @property + def is_chinese_char(self): + if self.formular: + return False + unicode = self.try_get_unicode() + if not unicode: + return False + if "(cid" in unicode: + return False + if len(unicode) > 1: + return False + assert len(unicode) == 1, "Unicode must be a single character" + if unicode in [ + "(", + ")", + "【", + "】", + "《", + "》", + "〔", + "〕", + "〈", + "〉", + "〖", + "〗", + "「", + "」", + "『", + "』", + "、", + "。", + ":", + "?", + "!", + ",", + ]: + return True + if unicode: + try: + unicodedata_name = unicodedata.name(unicode) + return ( + "CJK UNIFIED IDEOGRAPH" in unicodedata_name + or "FULLWIDTH" in unicodedata_name + ) + except ValueError: + return False + return False + + @property + def is_space(self): + if self.formular: + return False + unicode = self.try_get_unicode() + return unicode == " " + + @property + def is_hung_punctuation(self): + if self.formular: + return False + unicode = self.try_get_unicode() + + if unicode: + return unicode in [ + ",", + ".", + ":", + ";", + "?", + "!", + ",", + "。", + ":", + "?", + "!", + "]", + "}", + ")", + "〕", + "〉", + "】", + "〗", + "」", + "』", + "、", + "”", + '"', + ";", + ] + return False + + def passthrough(self) -> [PdfCharacter]: + if self.char: + return [self.char] + elif self.formular: + return self.formular.pdf_character + elif self.unicode: + logger.error(f"Cannot passthrough unicode. TypesettingUnit: {self}. ") + logger.error(f"Cannot passthrough unicode. TypesettingUnit: {self}. ") + return [] + + @property + def can_passthrough(self): + return self.unicode is None + + @property + def box(self): + if self.char: + return self.char.box + elif self.formular: + return self.formular.box + elif self.unicode: + char_width = self.font.char_lengths(self.unicode, self.font_size)[0] + if self.x is None or self.y is None or self.scale is None: + return Box(0, 0, char_width, self.font_size) + return Box(self.x, self.y, self.x + char_width, self.y + self.font_size) + + @property + def width(self): + return self.box.x2 - self.box.x + + @property + def height(self): + return self.box.y2 - self.box.y + + def relocate(self, x: float, y: float, scale: float) -> "TypesettingUnit": + """重定位并缩放排版单元 + + Args: + x: 新的 x 坐标 + y: 新的 y 坐标 + scale: 缩放因子 + + Returns: + 新的排版单元 + """ + if self.char: + # 创建新的字符对象 + new_char = PdfCharacter( + pdf_character_id=self.char.pdf_character_id, + char_unicode=self.char.char_unicode, + box=Box( + x=x, y=y, x2=x + self.width * scale, y2=y + self.height * scale + ), + pdf_style=PdfStyle( + font_id=self.char.pdf_style.font_id, + font_size=self.char.pdf_style.font_size * scale, + graphic_state=self.char.pdf_style.graphic_state, + ), + scale=scale, + vertical=self.char.vertical, + advance=self.char.advance * scale if self.char.advance else None, + debug_info=self.debug_info, + ) + return TypesettingUnit(char=new_char) + + elif self.formular: + # 创建新的公式对象,保持内部字符的相对位置 + new_chars = [] + min_x = min(char.box.x for char in self.formular.pdf_character) + min_y = min(char.box.y for char in self.formular.pdf_character) + + for char in self.formular.pdf_character: + # 计算相对位置 + rel_x = char.box.x - min_x + rel_y = char.box.y - min_y + + # 创建新的字符对象 + new_char = PdfCharacter( + pdf_character_id=char.pdf_character_id, + char_unicode=char.char_unicode, + box=Box( + x=x + (rel_x + self.formular.x_offset) * scale, + y=y + (rel_y + self.formular.y_offset) * scale, + x2=x + + (rel_x + (char.box.x2 - char.box.x) + self.formular.x_offset) + * scale, + y2=y + + (rel_y + (char.box.y2 - char.box.y) + self.formular.y_offset) + * scale, + ), + pdf_style=PdfStyle( + font_id=char.pdf_style.font_id, + font_size=char.pdf_style.font_size * scale, + graphic_state=char.pdf_style.graphic_state, + ), + scale=scale, + vertical=char.vertical, + advance=char.advance * scale if char.advance else None, + ) + new_chars.append(new_char) + + # Calculate bounding box from new_chars + min_x = min(char.box.x for char in new_chars) + min_y = min(char.box.y for char in new_chars) + max_x = max(char.box.x2 for char in new_chars) + max_y = max(char.box.y2 for char in new_chars) + + new_formula = PdfFormula( + box=Box(x=min_x, y=min_y, x2=max_x, y2=max_y), + pdf_character=new_chars, + x_offset=self.formular.x_offset * scale, + y_offset=self.formular.y_offset * scale, + ) + return TypesettingUnit(formular=new_formula) + + elif self.unicode: + # 对于 Unicode 字符,我们存储新的位置信息 + new_unit = TypesettingUnit( + unicode=self.unicode, + font=self.font, + font_size=self.font_size * scale, + style=self.style, + xobj_id=self.xobj_id, + debug_info=self.debug_info, + ) + new_unit.x = x + new_unit.y = y + new_unit.scale = scale + return new_unit + + def render(self) -> [PdfCharacter]: + """渲染排版单元为 PdfCharacter 列表 + + Returns: + PdfCharacter 列表 + """ + if self.can_passthrough: + return self.passthrough() + elif self.unicode: + assert self.x is not None, ( + "x position must be set, should be set by `relocate`" + ) + assert self.y is not None, ( + "y position must be set, should be set by `relocate`" + ) + assert self.scale is not None, ( + "scale must be set, should be set by `relocate`" + ) + # 计算字符宽度 + char_width = self.width + + new_char = PdfCharacter( + pdf_character_id=self.font.has_glyph(ord(self.unicode)), + char_unicode=self.unicode, + box=Box( + x=self.x, # 使用存储的位置 + y=self.y, + x2=self.x + char_width, + y2=self.y + self.font_size, + ), + pdf_style=PdfStyle( + font_id=self.font_id, + font_size=self.font_size, + graphic_state=self.style.graphic_state, + ), + scale=self.scale, + vertical=False, + advance=char_width, + xobj_id=self.xobj_id, + debug_info=self.debug_info, + ) + return [new_char] + else: + logger.error(f"Unknown typesetting unit. TypesettingUnit: {self}. ") + logger.error(f"Unknown typesetting unit. TypesettingUnit: {self}. ") + return [] + + +class Typesetting: + stage_name = "Typesetting" + + def __init__(self, translation_config: TranslationConfig): + self.font_mapper = FontMapper(translation_config) + self.translation_config = translation_config + + def typsetting_document(self, document: il_version_1.Document): + with self.translation_config.progress_monitor.stage_start( + self.stage_name, len(document.page) + ) as pbar: + for page in document.page: + self.translation_config.raise_if_cancelled() + self.render_page(page) + pbar.advance() + + def render_page(self, page: il_version_1.Page): + fonts: dict[ + str | int, il_version_1.PdfFont | dict[str, il_version_1.PdfFont] + ] = {f.font_id: f for f in page.pdf_font} + page_fonts = {f.font_id: f for f in page.pdf_font} + for k, v in self.font_mapper.fontid2font.items(): + fonts[k] = v + for xobj in page.pdf_xobject: + fonts[xobj.xobj_id] = page_fonts.copy() + for font in xobj.pdf_font: + fonts[xobj.xobj_id][font.font_id] = font + if page.page_number == 0: + self.add_watermark(page) + # 开始实际的渲染过程 + for paragraph in page.pdf_paragraph: + self.render_paragraph(paragraph, page, fonts) + + def add_watermark(self, page: il_version_1.Page): + page_width = page.cropbox.box.x2 - page.cropbox.box.x + page_height = page.cropbox.box.y2 - page.cropbox.box.y + style = il_version_1.PdfStyle( + font_id="base", font_size=6, graphic_state=il_version_1.GraphicState() + ) + text = "" + if self.translation_config.debug: + text += "\n 当前为DEBUG模式,将显示更多辅助信息。请注意,部分框的位置对应原文,但在译文中可能不正确。" + page.pdf_paragraph.append( + il_version_1.PdfParagraph( + first_line_indent=False, + box=il_version_1.Box( + x=page.cropbox.box.x + page_width * 0.05, + y=page.cropbox.box.y, + x2=page.cropbox.box.x2, + y2=page.cropbox.box.y2 - page_height * 0.05, + ), + vertical=False, + pdf_style=style, + pdf_paragraph_composition=[ + il_version_1.PdfParagraphComposition( + pdf_same_style_unicode_characters=il_version_1.PdfSameStyleUnicodeCharacters( + unicode=text, pdf_style=style + ) + ) + ], + xobj_id=-1, + ) + ) + + def render_paragraph( + self, + paragraph: il_version_1.PdfParagraph, + page: il_version_1.Page, + fonts: dict[str | int, il_version_1.PdfFont | dict[str, il_version_1.PdfFont]], + ): + typesetting_units = self.create_typesetting_units(paragraph, fonts) + # 如果所有单元都可以直接传递,则直接传递 + if all(unit.can_passthrough for unit in typesetting_units): + paragraph.scale = 1.0 + paragraph.pdf_paragraph_composition = self.create_passthrough_composition( + typesetting_units + ) + return + + # 如果有单元无法直接传递,则进行重排版 + paragraph.pdf_paragraph_composition = [] + self.retypeset(paragraph, page, typesetting_units) + + def _layout_typesetting_units( + self, + typesetting_units: list[TypesettingUnit], + box: Box, + scale: float, + line_spacing: float, + paragraph: il_version_1.PdfParagraph, + ) -> tuple[list[TypesettingUnit], bool]: + """布局排版单元。 + + Args: + typesetting_units: 要布局的排版单元列表 + box: 布局边界框 + scale: 缩放因子 + line_spacing: 行间距 + + Returns: + tuple[list[TypesettingUnit], bool]: (已布局的排版单元列表, 是否所有单元都放得下) + """ + # 计算字号众数 + font_sizes = [] + for unit in typesetting_units: + if getattr(unit, "font_size", None): + font_sizes.append(unit.font_size) + if getattr(unit, "char", None): + font_sizes.append(unit.char.pdf_style.font_size) + font_sizes.sort() + font_size = statistics.mode(font_sizes) + + space_width = ( + self.font_mapper.base_font.char_lengths("你", font_size * scale)[0] * 0.5 + ) + + # 计算平均行高 + avg_height = ( + sum(unit.height * scale for unit in typesetting_units) + / len(typesetting_units) + if typesetting_units + else 0 + ) + + # 初始化位置为右上角,并减去一个平均行高 + current_x = box.x + current_y = box.y2 - avg_height + line_height = 0 + + # 存储已排版的单元 + typeset_units = [] + all_units_fit = True + last_unit: TypesettingUnit | None = None + + if paragraph.first_line_indent: + current_x += space_width * 4 + # 遍历所有排版单元 + for unit in typesetting_units: + # 计算当前单元在当前缩放下的尺寸 + unit_width = unit.width * scale + unit_height = unit.height * scale + + # 跳过行首的空格 + if current_x == box.x and unit.is_space: + continue + + if ( + last_unit # 有上一个单元 + and last_unit.is_chinese_char ^ unit.is_chinese_char # 中英文交界处 + and ( + last_unit.box + and last_unit.box.y + and current_y - 0.1 + <= last_unit.box.y2 + <= current_y + line_height + 0.1 + ) # 在同一行,且有垂直重叠 + and not last_unit.mixed_character_blacklist # 不是混排空格黑名单字符 + and not unit.mixed_character_blacklist # 同上 + and current_x > box.x # 不是行首 + and unit.try_get_unicode() != " " # 不是空格 + and last_unit.try_get_unicode() != " " # 不是空格 + ): + current_x += space_width * 0.5 + + # 如果当前行放不下这个元素,换行 + if current_x + unit_width > box.x2 and not unit.is_hung_punctuation: + # 换行 + current_x = box.x + current_y -= line_height * line_spacing + line_height = 0.0 + + # 检查是否超出底部边界 + if current_y - unit_height < box.y: + all_units_fit = False + break + + if unit.is_space: + line_height = max(line_height, unit_height) + continue + + # 放置当前单元 + relocated_unit = unit.relocate(current_x, current_y, scale) + typeset_units.append(relocated_unit) + + # workaround: 超长行距暂时没找到具体原因,有待进一步修复。这里的1.2是魔法数字! + # 更新当前行的最大高度 + if line_height == 0 or line_height * 1.2 > unit_height > line_height: + line_height = unit_height + + # 更新 x 坐标 + current_x = relocated_unit.box.x2 + + last_unit = relocated_unit + + return typeset_units, all_units_fit + + def retypeset( + self, + paragraph: il_version_1.PdfParagraph, + page: il_version_1.Page, + typesetting_units: list[TypesettingUnit], + ): + box = paragraph.box + scale = 1.0 + line_spacing = 1.5 # 初始行距为1.7 + min_scale = 0.1 # 最小缩放因子 + min_line_spacing = 1.4 # 最小行距 + expand_space_flag = False + + while scale >= min_scale: + # 尝试布局排版单元 + typeset_units, all_units_fit = self._layout_typesetting_units( + typesetting_units, box, scale, line_spacing, paragraph + ) + + # 如果所有单元都放得下,就完成排版 + if all_units_fit: + # 将排版后的单元转换为段落组合 + paragraph.scale = scale + paragraph.pdf_paragraph_composition = [] + for unit in typeset_units: + for char in unit.render(): + paragraph.pdf_paragraph_composition.append( + PdfParagraphComposition(pdf_character=char) + ) + return + + if not expand_space_flag: + # 如果尚未扩展空格,进行扩展 + max_x = self.get_max_right_space(box, page) + # 只有当有额外空间时才扩展 + if max_x > box.x2: + expanded_box = Box( + x=box.x, + y=box.y, + x2=max_x, # 直接扩展到最大可用位置 + y2=box.y2, + ) + # 更新段落的边界框 + paragraph.box = expanded_box + expand_space_flag = True + continue + + # 如果当前行距大于最小行距,先减小行距 + if line_spacing > min_line_spacing: + line_spacing -= 0.1 + else: + # 行距已经最小,减小缩放因子 + if scale > 0.6: + scale -= 0.05 + else: + scale -= 0.1 + line_spacing = 1.5 # 重置行距 + + if scale < 0.7 and min_line_spacing > 1.1: + min_line_spacing = 1.1 + scale = 1.0 + line_spacing = 1.5 + + def create_typesetting_units( + self, + paragraph: il_version_1.PdfParagraph, + fonts: dict[str, il_version_1.PdfFont], + ) -> list[TypesettingUnit]: + if not paragraph.pdf_paragraph_composition: + return [] + result = [] + + @cache + def get_font(font_id: str, xobj_id: int): + if xobj_id in fonts: + font = fonts[xobj_id][font_id] + else: + font = fonts[font_id] + return font + + for composition in paragraph.pdf_paragraph_composition: + if composition is None: + continue + if composition.pdf_line: + result.extend( + [ + TypesettingUnit(char=char) + for char in composition.pdf_line.pdf_character + ] + ) + elif composition.pdf_character: + result.append( + TypesettingUnit( + char=composition.pdf_character, debug_info=paragraph.debug_info + ) + ) + elif composition.pdf_same_style_characters: + result.extend( + [ + TypesettingUnit(char=char) + for char in composition.pdf_same_style_characters.pdf_character + ] + ) + elif composition.pdf_same_style_unicode_characters: + font_id = ( + composition.pdf_same_style_unicode_characters.pdf_style.font_id + ) + font = get_font(font_id, paragraph.xobj_id) + result.extend( + [ + TypesettingUnit( + unicode=char_unicode, + font=self.font_mapper.map(font, char_unicode), + font_size=composition.pdf_same_style_unicode_characters.pdf_style.font_size, + style=composition.pdf_same_style_unicode_characters.pdf_style, + xobj_id=paragraph.xobj_id, + debug_info=composition.pdf_same_style_unicode_characters.debug_info, + ) + for char_unicode in composition.pdf_same_style_unicode_characters.unicode + if char_unicode not in ("\n",) + ] + ) + elif composition.pdf_formula: + result.extend([TypesettingUnit(formular=composition.pdf_formula)]) + else: + logger.error( + f"Unknown composition type. " + f"Composition: {composition}. " + f"Paragraph: {paragraph}. " + ) + continue + result = list(filter(lambda x: x.unicode is None or x.font is not None, result)) + return result + + def create_passthrough_composition( + self, typesetting_units: list[TypesettingUnit] + ) -> list[PdfParagraphComposition]: + """从排版单元创建直接传递的段落组合。 + + Args: + typesetting_units: 排版单元列表 + + Returns: + 段落组合列表 + """ + composition = [] + for unit in typesetting_units: + composition.extend( + [ + PdfParagraphComposition(pdf_character=char) + for char in unit.passthrough() + ] + ) + return composition + + def get_max_right_space(self, current_box: Box, page) -> float: + """获取段落右侧最大可用空间 + + Args: + current_box: 当前段落的边界框 + page: 当前页面 + + Returns: + 可以扩展到的最大 x 坐标 + """ + # TODO: try to find right margin of page + # 获取页面的裁剪框作为初始最大限制 + max_x = page.cropbox.box.x2 * 0.9 + + # 检查所有可能的阻挡元素 + for para in page.pdf_paragraph: + if para.box == current_box or para.box is None: # 跳过当前段落 + continue + # 只考虑在当前段落右侧且有垂直重叠的元素 + if para.box.x > current_box.x and not ( + para.box.y >= current_box.y2 or para.box.y2 <= current_box.y + ): + max_x = min(max_x, para.box.x) + + # 检查图形 + for figure in page.pdf_figure: + if figure.box.x > current_box.x and not ( + figure.box.y >= current_box.y2 or figure.box.y2 <= current_box.y + ): + max_x = min(max_x, figure.box.x) + + return max_x diff --git a/src/pdf2u/document_il/translator/__init__.py b/src/pdf2u/document_il/translator/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/pdf2u/document_il/translator/__pycache__/__init__.cpython-311.pyc b/src/pdf2u/document_il/translator/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b53ce0a533be8bafdf8ca950844149850d36911c Binary files /dev/null and b/src/pdf2u/document_il/translator/__pycache__/__init__.cpython-311.pyc differ diff --git a/src/pdf2u/document_il/translator/__pycache__/__init__.cpython-312.pyc b/src/pdf2u/document_il/translator/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1eb644b6b43de6e51b2ceb68bba415cb6255c76c Binary files /dev/null and b/src/pdf2u/document_il/translator/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/pdf2u/document_il/translator/__pycache__/cache.cpython-311.pyc b/src/pdf2u/document_il/translator/__pycache__/cache.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..792fff9ba5d9ba8ea62f982d2e34c91f0de730ea Binary files /dev/null and b/src/pdf2u/document_il/translator/__pycache__/cache.cpython-311.pyc differ diff --git a/src/pdf2u/document_il/translator/__pycache__/cache.cpython-312.pyc b/src/pdf2u/document_il/translator/__pycache__/cache.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..520c66c9496b692c1db90477c0c0b337ad96e37a Binary files /dev/null and b/src/pdf2u/document_il/translator/__pycache__/cache.cpython-312.pyc differ diff --git a/src/pdf2u/document_il/translator/__pycache__/translator.cpython-311.pyc b/src/pdf2u/document_il/translator/__pycache__/translator.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2c509b3a2d87ae38f5e2332c3364df7a28f82191 Binary files /dev/null and b/src/pdf2u/document_il/translator/__pycache__/translator.cpython-311.pyc differ diff --git a/src/pdf2u/document_il/translator/__pycache__/translator.cpython-312.pyc b/src/pdf2u/document_il/translator/__pycache__/translator.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d54b2a4f1599ba96a7ff29060c2dcf73b7aee43d Binary files /dev/null and b/src/pdf2u/document_il/translator/__pycache__/translator.cpython-312.pyc differ diff --git a/src/pdf2u/document_il/translator/cache.py b/src/pdf2u/document_il/translator/cache.py new file mode 100644 index 0000000000000000000000000000000000000000..43e6b9190ee638926082108f36632d58ba534984 --- /dev/null +++ b/src/pdf2u/document_il/translator/cache.py @@ -0,0 +1,131 @@ +import json +from pathlib import Path + +from peewee import SQL, AutoField, CharField, Model, SqliteDatabase, TextField + +from pdf2u.const import CACHE_FOLDER as cache_folder + +# we don't init the database here +db = SqliteDatabase(None) + + +class _TranslationCache(Model): + id = AutoField() + translate_engine = CharField(max_length=20) + translate_engine_params = TextField() + original_text = TextField() + translation = TextField() + + class Meta: + database = db + constraints = [ + SQL( + """ + UNIQUE ( + translate_engine, + translate_engine_params, + original_text + ) + ON CONFLICT REPLACE + """ + ) + ] + + +class TranslationCache: + @staticmethod + def _sort_dict_recursively(obj): + if isinstance(obj, dict): + return { + k: TranslationCache._sort_dict_recursively(v) + for k in sorted(obj.keys()) + for v in [obj[k]] + } + elif isinstance(obj, list): + return [TranslationCache._sort_dict_recursively(item) for item in obj] + return obj + + def __init__(self, translate_engine: str, translate_engine_params: dict = None): + self.translate_engine = translate_engine + self.replace_params(translate_engine_params) + + # The program typically starts multi-threaded translation + # only after cache parameters are fully configured, + # so thread safety doesn't need to be considered here. + def replace_params(self, params: dict = None): + if params is None: + params = {} + self.params = params + params = self._sort_dict_recursively(params) + self.translate_engine_params = json.dumps(params) + + def update_params(self, params: dict = None): + if params is None: + params = {} + self.params.update(params) + self.replace_params(self.params) + + def add_params(self, k: str, v): + self.params[k] = v + self.replace_params(self.params) + + # Since peewee and the underlying sqlite are thread-safe, + # get and set operations don't need locks. + def get(self, original_text: str) -> str | None: + result = _TranslationCache.get_or_none( + translate_engine=self.translate_engine, + translate_engine_params=self.translate_engine_params, + original_text=original_text, + ) + return result.translation if result else None + + def set(self, original_text: str, translation: str): + _TranslationCache.create( + translate_engine=self.translate_engine, + translate_engine_params=self.translate_engine_params, + original_text=original_text, + translation=translation, + ) + + +def init_db(remove_exists=False): + cache_folder.mkdir(parents=True, exist_ok=True) + # The current version does not support database migration, so add the version number to the file name. + cache_db_path = cache_folder / "cache.v1.db" + if remove_exists and cache_db_path.exists(): + cache_db_path.unlink() + db.init(cache_db_path, pragmas={"journal_mode": "wal", "busy_timeout": 1000}) + db.create_tables([_TranslationCache], safe=True) + + +def init_test_db(): + import tempfile + + temp_file = tempfile.NamedTemporaryFile(suffix=".db", delete=False) + cache_db_path = temp_file.name + temp_file.close() + + test_db = SqliteDatabase( + cache_db_path, pragmas={"journal_mode": "wal", "busy_timeout": 1000} + ) + test_db.bind([_TranslationCache], bind_refs=False, bind_backrefs=False) + test_db.connect() + test_db.create_tables([_TranslationCache], safe=True) + return test_db + + +def clean_test_db(test_db): + test_db.drop_tables([_TranslationCache]) + test_db.close() + db_path = Path(test_db.database) + if db_path.exists(): + db_path.unlink() + wal_path = Path(str(db_path) + "-wal") + if wal_path.exists(): + wal_path.unlink() + shm_path = Path(str(db_path) + "-shm") + if shm_path.exists(): + shm_path.unlink() + + +init_db() diff --git a/src/pdf2u/document_il/translator/translator.py b/src/pdf2u/document_il/translator/translator.py new file mode 100644 index 0000000000000000000000000000000000000000..7def46def20c469d6a9193620dc09a2fc4ddacdb --- /dev/null +++ b/src/pdf2u/document_il/translator/translator.py @@ -0,0 +1,253 @@ +import html +import logging +import re +import threading +import time +import unicodedata +from abc import ABC + +import openai +import requests + +from pdf2u.document_il.translator.cache import TranslationCache + +logger = logging.getLogger(__name__) + + +def remove_control_characters(s): + return "".join(ch for ch in s if unicodedata.category(ch)[0] != "C") + + +class RateLimiter: + def __init__(self, max_qps: int): + self.max_qps = max_qps + self.min_interval = 1.0 / max_qps + self.last_requests = [] # Track last N requests + self.window_size = max_qps # Track requests in a sliding window + self.lock = threading.Lock() + + def wait(self): + with self.lock: + now = time.time() + + # Clean up old requests outside the 1-second window + while self.last_requests and now - self.last_requests[0] > 1.0: + self.last_requests.pop(0) + + # If we have less than max_qps requests in the last second, allow immediately + if len(self.last_requests) < self.max_qps: + self.last_requests.append(now) + return + + # Otherwise, wait until we can make the next request + next_time = self.last_requests[0] + 1.0 + if next_time > now: + time.sleep(next_time - now) + self.last_requests.pop(0) + self.last_requests.append(next_time) + + def set_max_qps(self, max_qps) -> None: + self.max_qps = max_qps + self.min_interval = 1.0 / max_qps + self.window_size = max_qps + + +_translate_rate_limiter = RateLimiter(5) + + +def set_translate_rate_limiter(max_qps: int) -> None: + _translate_rate_limiter.set_max_qps(max_qps) + + +class BaseTranslator(ABC): + # Due to cache limitations, name should be within 20 characters. + # cache.py: translate_engine = CharField(max_length=20) + name = "base" + lang_map = {} + + def __init__(self, lang_in, lang_out, ignore_cache): + self.ignore_cache = ignore_cache + lang_in = self.lang_map.get(lang_in.lower(), lang_in) + lang_out = self.lang_map.get(lang_out.lower(), lang_out) + self.lang_in = lang_in + self.lang_out = lang_out + + self.cache = TranslationCache( + self.name, {"lang_in": lang_in, "lang_out": lang_out} + ) + + self.translate_call_count = 0 + self.translate_cache_call_count = 0 + + def __del__(self): + logger.info(f"{self.name} translate call count: {self.translate_call_count}") + logger.info( + f"{self.name} translate cache call count: {self.translate_cache_call_count}" + ) + + def add_cache_impact_parameters(self, k: str, v): + """ + Add parameters that affect the translation quality to distinguish the translation effects under different parameters. + :param k: key + :param v: value + """ + self.cache.add_params(k, v) + + def translate(self, text, ignore_cache=False): + """ + Translate the text, and the other part should call this method. + :param text: text to translate + :return: translated text + """ + self.translate_call_count += 1 + if not (self.ignore_cache or ignore_cache): + cache = self.cache.get(text) + if cache is not None: + self.translate_cache_call_count += 1 + return cache + _translate_rate_limiter.wait() + translation = self.do_translate(text) + if not (self.ignore_cache or ignore_cache): + self.cache.set(text, translation) + return translation + + def do_translate(self, text): + """ + Actual translate text, override this method + :param text: text to translate + :return: translated text + """ + logger.critical( + f"Do not call BaseTranslator.do_translate. " + f"Translator: {self}. " + f"Text: {text}. " + ) + raise NotImplementedError + + def __str__(self): + return f"{self.name} {self.lang_in} {self.lang_out} {self.model}" + + def get_rich_text_left_placeholder(self, placeholder_id: int): + return f"" + + def get_rich_text_right_placeholder(self, placeholder_id: int): + return f"" + + def get_formular_placeholder(self, placeholder_id: int): + return self.get_rich_text_left_placeholder(placeholder_id) + + +class GoogleTranslator(BaseTranslator): + name = "google" + lang_map = {"zh": "zh-CN"} + + def __init__(self, lang_in, lang_out, ignore_cache=False) -> None: + super().__init__(lang_in, lang_out, ignore_cache) + self.session = requests.Session() + self.endpoint = "http://translate.google.com/m" + self.headers = { + "User-Agent": "Mozilla/4.0 (compatible;MSIE 6.0;Windows NT 5.1;SV1;.NET CLR 1.1.4322;.NET CLR 2.0.50727;.NET CLR 3.0.04506.30)" + } + + def do_translate(self, text): + text = text[:5000] # google translate max length + response = self.session.get( + self.endpoint, + params={"tl": self.lang_out, "sl": self.lang_in, "q": text}, + headers=self.headers, + ) + re_result = re.findall( + r'(?s)class="(?:t0|result-container)">(.*?)<', response.text + ) + if response.status_code == 400: + result = "IRREPARABLE TRANSLATION ERROR" + else: + response.raise_for_status() + result = html.unescape(re_result[0]) + return remove_control_characters(result) + + +class BingTranslator(BaseTranslator): + # https://github.com/immersive-translate/old-immersive-translate/blob/6df13da22664bea2f51efe5db64c63aca59c4e79/src/background/translationService.js + name = "bing" + lang_map = {"zh": "zh-Hans"} + + def __init__(self, lang_in, lang_out, ignore_cache=False) -> None: + super().__init__(lang_in, lang_out, ignore_cache) + self.session = requests.Session() + self.endpoint = "https://www.bing.com/translator" + self.headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0" + } + + def find_sid(self): + response = self.session.get(self.endpoint) + response.raise_for_status() + url = response.url[:-10] + ig = re.findall(r"\"ig\":\"(.*?)\"", response.text)[0] + iid = re.findall(r"data-iid=\"(.*?)\"", response.text)[-1] + key, token = re.findall( + r"params_AbusePreventionHelper\s=\s\[(.*?),\"(.*?)\",", response.text + )[0] + return url, ig, iid, key, token + + def do_translate(self, text): + text = text[:1000] # bing translate max length + url, ig, iid, key, token = self.find_sid() + response = self.session.post( + f"{url}ttranslatev3?IG={ig}&IID={iid}", + data={ + "fromLang": self.lang_in, + "to": self.lang_out, + "text": text, + "token": token, + "key": key, + }, + headers=self.headers, + ) + response.raise_for_status() + return response.json()[0]["translations"][0]["text"] + + +class OpenAITranslator(BaseTranslator): + # https://github.com/openai/openai-python + name = "openai" + + def __init__( + self, lang_in, lang_out, model, base_url=None, api_key=None, ignore_cache=False + ) -> None: + super().__init__(lang_in, lang_out, ignore_cache) + self.options = {"temperature": 0} # 随机采样可能会打断公式标记 + self.client = openai.OpenAI(base_url=base_url, api_key=api_key) + self.add_cache_impact_parameters("temperature", self.options["temperature"]) + self.model = model + self.add_cache_impact_parameters("model", self.model) + self.add_cache_impact_parameters("prompt", self.prompt("")) + + def do_translate(self, text) -> str: + response = self.client.chat.completions.create( + model=self.model, **self.options, messages=self.prompt(text) + ) + return response.choices[0].message.content.strip() + + def prompt(self, text): + return [ + { + "role": "system", + "content": "You are a professional,authentic machine translation engine.", + }, + { + "role": "user", + "content": f";; Treat next line as plain text input and translate it into {self.lang_out}, output translation ONLY. If translation is unnecessary (e.g. proper nouns, codes, {'{{1}}, etc. '}), return the original text. NO explanations. NO notes. Input:\n\n{text}", + }, + ] + + def get_formular_placeholder(self, placeholder_id: int): + return "{{v" + str(placeholder_id) + "}}" + return "{{" + str(placeholder_id) + "}}" + + def get_rich_text_left_placeholder(self, placeholder_id: int): + return self.get_formular_placeholder(placeholder_id) + + def get_rich_text_right_placeholder(self, placeholder_id: int): + return self.get_formular_placeholder(placeholder_id + 1) diff --git a/src/pdf2u/document_il/utils/__pycache__/fontmap.cpython-311.pyc b/src/pdf2u/document_il/utils/__pycache__/fontmap.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5ccc080bbc90d5884ad531bf7b4756e05b2ca827 Binary files /dev/null and b/src/pdf2u/document_il/utils/__pycache__/fontmap.cpython-311.pyc differ diff --git a/src/pdf2u/document_il/utils/__pycache__/fontmap.cpython-312.pyc b/src/pdf2u/document_il/utils/__pycache__/fontmap.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b833b50275317a8521245781c98084b57e6fd251 Binary files /dev/null and b/src/pdf2u/document_il/utils/__pycache__/fontmap.cpython-312.pyc differ diff --git a/src/pdf2u/document_il/utils/__pycache__/layout_helper.cpython-311.pyc b/src/pdf2u/document_il/utils/__pycache__/layout_helper.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..564523f4670288315224811ad0dc5c3f72f95b1e Binary files /dev/null and b/src/pdf2u/document_il/utils/__pycache__/layout_helper.cpython-311.pyc differ diff --git a/src/pdf2u/document_il/utils/__pycache__/layout_helper.cpython-312.pyc b/src/pdf2u/document_il/utils/__pycache__/layout_helper.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ae096ec957494bb46a40ffdd0db82754dd8bba2a Binary files /dev/null and b/src/pdf2u/document_il/utils/__pycache__/layout_helper.cpython-312.pyc differ diff --git a/src/pdf2u/document_il/utils/__pycache__/style_helper.cpython-311.pyc b/src/pdf2u/document_il/utils/__pycache__/style_helper.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e0bca6bf8348d71fae106269459aed8ed023b016 Binary files /dev/null and b/src/pdf2u/document_il/utils/__pycache__/style_helper.cpython-311.pyc differ diff --git a/src/pdf2u/document_il/utils/__pycache__/style_helper.cpython-312.pyc b/src/pdf2u/document_il/utils/__pycache__/style_helper.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1bdc16c4831e0eb091b5236833cbb1c97f3e02ad Binary files /dev/null and b/src/pdf2u/document_il/utils/__pycache__/style_helper.cpython-312.pyc differ diff --git a/src/pdf2u/document_il/utils/fontmap.py b/src/pdf2u/document_il/utils/fontmap.py new file mode 100644 index 0000000000000000000000000000000000000000..df0b449bf160a7a378c361c296623cbc89a09abb --- /dev/null +++ b/src/pdf2u/document_il/utils/fontmap.py @@ -0,0 +1,224 @@ +import functools +import logging +import re +from pathlib import Path + +import pymupdf + +from pdf2u.const import get_cache_file_path +from pdf2u.document_il import PdfFont, il_version_1 +from pdf2u.translation_config import TranslationConfig + +logger = logging.getLogger(__name__) + + +class FontMapper: + stage_name = "Add Fonts" + + def __init__(self, translation_config: TranslationConfig): + self.font_names = [ + "source-han-serif-cn.ttf", + "SourceHanSansSC-Regular.ttf", + "source-han-serif-cn-bold.ttf", + "SourceHanSansSC-Bold.ttf", + ] + self.fonts = { + Path(file_name).name.split(".")[0].replace("-", "").lower(): pymupdf.Font( + fontfile=str(get_cache_file_path(file_name)) + ) + for file_name in self.font_names + } + for k, v in self.fonts.items(): + v.font_id = k + self.translation_config = translation_config + self.base_font_path = translation_config.font + self.fallback_font_path = get_cache_file_path("noto.ttf") + self.base_font = pymupdf.Font(fontfile=str(self.base_font_path)) + self.fallback_font = pymupdf.Font(fontfile=str(self.fallback_font_path)) + + self.kai_font_path = get_cache_file_path("LXGWWenKai-Regular.ttf") + self.kai_font = pymupdf.Font(fontfile=str(self.kai_font_path)) + + self.base_font.font_id = "base" + self.fallback_font.font_id = "fallback" + self.kai_font.font_id = "kai" + + # Set ascent and descent for base font + self.base_font.ascent_fontmap = 1151 + self.base_font.descent_fontmap = -286 + + # Set ascent and descent for fallback font + self.fallback_font.ascent_fontmap = 1069 + self.fallback_font.descent_fontmap = -293 + + # Set ascent and descent for kai font + self.kai_font.ascent_fontmap = 928 + self.kai_font.descent_fontmap = -256 + + self.fontid2font = {f.font_id: f for f in self.fonts.values()} + self.fontid2font["base"] = self.base_font + self.fontid2font["fallback"] = self.fallback_font + self.fontid2font["kai"] = self.kai_font + + # Set ascent and descent for other fonts + font_metrics = { + "sourcehanserifcn": (1151, 0), + "sourcehansansscregular": (1160, -288), + "sourcehanserifcnbold": (1151, -286), + "sourcehansansscbold": (1160, -288), + } + + for font_id, (ascent, descent) in font_metrics.items(): + if font_id in self.fontid2font: + self.fontid2font[font_id].ascent_fontmap = ascent + self.fontid2font[font_id].descent_fontmap = descent + + for font in self.fontid2font.values(): + font.char_lengths = functools.lru_cache(maxsize=10240, typed=True)( + font.char_lengths + ) + + def has_char(self, char_unicode: str): + if len(char_unicode) != 1: + return False + current_char = ord(char_unicode) + for font in self.fonts.values(): + if font.has_glyph(current_char): + return True + if self.base_font.has_glyph(current_char): + return True + if self.fallback_font.has_glyph(current_char): + return True + return False + + def map(self, original_font: PdfFont, char_unicode: str): + current_char = ord(char_unicode) + if isinstance(original_font, pymupdf.Font): + bold = original_font.is_bold + italic = original_font.is_italic + monospaced = original_font.is_monospaced + serif = original_font.is_serif + elif isinstance(original_font, PdfFont): + bold = original_font.bold + italic = original_font.italic + monospaced = original_font.monospace + serif = original_font.serif + else: + logger.error( + f"Unknown font type: {type(original_font)}. " + f"Original font: {original_font}. " + f"Char unicode: {char_unicode}. " + ) + return None + if italic and self.kai_font.has_glyph(current_char): + return self.kai_font + for _k, font in self.fonts.items(): + if not font.has_glyph(current_char): + continue + if bold != font.is_bold: + continue + # 不知道什么原因,思源黑体的 serif 属性为1,先workaround + if serif == 1 and "serif" not in font.font_id: + continue + if serif == 0 and "serif" in font.font_id: + continue + return font + if self.base_font.has_glyph(current_char): + return self.base_font + + if self.fallback_font.has_glyph(current_char): + return self.fallback_font + + logger.error( + f"Can't find font for {char_unicode}({current_char}). " + f"Original font: {original_font}. " + f"Char unicode: {char_unicode}. " + ) + return None + + def add_font(self, doc_zh: pymupdf.Document, il: il_version_1.Document): + font_list = [ + ("base", self.base_font_path), + ("fallback", self.fallback_font_path), + ("kai", self.kai_font_path), + ] + font_list.extend( + [ + ( + Path(file_name).name.split(".")[0].replace("-", "").lower(), + get_cache_file_path(file_name), + ) + for file_name in self.font_names + ] + ) + font_id = {} + xreflen = doc_zh.xref_length() + with self.translation_config.progress_monitor.stage_start( + self.stage_name, + xreflen - 1 + len(font_list) + len(il.page) + len(font_list), + ) as pbar: + for font in font_list: + font_id[font[0]] = doc_zh[0].insert_font(font[0], font[1]) + pbar.advance(1) + for xref in range(1, xreflen): + pbar.advance(1) + for label in ["Resources/", ""]: # 可能是基于 xobj 的 res + try: # xref 读写可能出错 + font_res = doc_zh.xref_get_key(xref, f"{label}Font") + if font_res is None: + continue + target_key_prefix = f"{label}Font/" + if font_res[0] == "xref": + resource_xref_id = re.search( + "(\\d+) 0 R", font_res[1] + ).group(1) + xref = int(resource_xref_id) + font_res = ("dict", doc_zh.xref_object(xref)) + target_key_prefix = "" + if font_res[0] == "dict": + for font in font_list: + target_key = f"{target_key_prefix}{font[0]}" + font_exist = doc_zh.xref_get_key(xref, target_key) + if font_exist[0] == "null": + doc_zh.xref_set_key( + xref, target_key, f"{font_id[font[0]]} 0 R" + ) + except Exception: + pass + + # Create PdfFont for each font + # 预先创建所有字体对象 + pdf_fonts = [] + for font_name, font_path in font_list: + font = pymupdf.Font(fontfile=str(font_path)) + # Get descent_fontmap from fontid2font + descent_fontmap = None + if font_name in self.fontid2font: + mupdf_font = self.fontid2font[font_name] + if hasattr(mupdf_font, "descent_fontmap"): + descent_fontmap = mupdf_font.descent_fontmap + if hasattr(mupdf_font, "ascent_fontmap"): + ascent_fontmap = mupdf_font.ascent_fontmap + + pdf_fonts.append( + il_version_1.PdfFont( + name=font_name, + xref_id=font_id[font_name], + font_id=font_name, + encoding_length=2, + bold=font.is_bold, + italic=font.is_italic, + monospace=font.is_monospaced, + serif=font.is_serif, + descent=descent_fontmap, + ascent=ascent_fontmap, + ) + ) + pbar.advance(1) + + # 批量添加字体到页面和XObject + for page in il.page: + page.pdf_font.extend(pdf_fonts) + for xobj in page.pdf_xobject: + xobj.pdf_font.extend(pdf_fonts) + pbar.advance(1) diff --git a/src/pdf2u/document_il/utils/layout_helper.py b/src/pdf2u/document_il/utils/layout_helper.py new file mode 100644 index 0000000000000000000000000000000000000000..2cb5a48e1799873e707d08c08cbce3e9f32759a9 --- /dev/null +++ b/src/pdf2u/document_il/utils/layout_helper.py @@ -0,0 +1,465 @@ +import logging +import math + +from pymupdf import Font + +from pdf2u.document_il import GraphicState +from pdf2u.document_il.il_version_1 import ( + Box, + PdfCharacter, + PdfParagraph, + PdfParagraphComposition, +) + +logger = logging.getLogger(__name__) +HEIGHT_NOT_USFUL_CHAR_IN_CHAR = ( + "∑︁", + # 暂时假设cid:17和cid 16是特殊情况 + # 来源于 arXiv:2310.18608v2 第九页公式大括号 + "(cid:17)", + "(cid:16)", + # arXiv:2411.19509v2 第四页 [] + "(cid:104)", + "(cid:105)", + # arXiv:2411.19509v2 第四页 公式的|竖线 + "(cid:13)", + "∑︁", + # arXiv:2412.05265 27页 累加号 + "(cid:88)", + # arXiv:2412.05265 16页 累乘号 + "(cid:89)", + # arXiv:2412.05265 27页 积分 + "(cid:90)", + # arXiv:2412.05265 32页 公式左右的中括号 + "(cid:2)", + "(cid:3)", +) + + +LEFT_BRACKET = ("(cid:8)", "(", "(cid:16)", "{", "[", "(cid:104)", "(cid:2)") +RIGHT_BRACKET = ("(cid:9)", ")", "(cid:17)", "}", "]", "(cid:105)", "(cid:3)") + + +def formular_height_ignore_char(char: PdfCharacter): + return ( + char.pdf_character_id is None + or char.char_unicode in HEIGHT_NOT_USFUL_CHAR_IN_CHAR + ) + + +class Layout: + def __init__(self, layout_id, name): + self.id = layout_id + self.name = name + + @staticmethod + def is_newline(prev_char: PdfCharacter, curr_char: PdfCharacter) -> bool: + # 如果没有前一个字符,不是换行 + if prev_char is None: + return False + + # 获取两个字符的中心 y 坐标 + # prev_y = (prev_char.box.y + prev_char.box.y2) / 2 + # curr_y = (curr_char.box.y + curr_char.box.y2) / 2 + + # 如果当前字符的 y 坐标明显低于前一个字符,说明换行了 + # 这里使用字符高度的一半作为阈值 + char_height = max( + curr_char.box.y2 - curr_char.box.y, prev_char.box.y2 - prev_char.box.y + ) + char_width = max( + curr_char.box.x2 - curr_char.box.x, prev_char.box.x2 - prev_char.box.x + ) + should_new_line = ( + curr_char.box.y2 < prev_char.box.y + or curr_char.box.x2 < prev_char.box.x - char_width * 10 + ) + if should_new_line and ( + formular_height_ignore_char(curr_char) + or formular_height_ignore_char(prev_char) + ): + return False + return should_new_line + + +def get_paragraph_length_except( + paragraph: PdfParagraph, except_chars: str, font: Font +) -> int: + length = 0 + for composition in paragraph.pdf_paragraph_composition: + if composition.pdf_character: + length += ( + composition.pdf_character[0].box.x2 - composition.pdf_character[0].box.x + ) + elif composition.pdf_same_style_characters: + for pdf_char in composition.pdf_same_style_characters.pdf_character: + if pdf_char.char_unicode in except_chars: + continue + length += pdf_char.box.x2 - pdf_char.box.x + elif composition.pdf_same_style_unicode_characters: + for char_unicode in composition.pdf_same_style_unicode_characters.unicode: + if char_unicode in except_chars: + continue + length += font.char_lengths( + char_unicode, + composition.pdf_same_style_unicode_characters.pdf_style.font_size, + )[0] + elif composition.pdf_line: + for pdf_char in composition.pdf_line.pdf_character: + if pdf_char.char_unicode in except_chars: + continue + length += pdf_char.box.x2 - pdf_char.box.x + elif composition.pdf_formula: + length += composition.pdf_formula.box.x2 - composition.pdf_formula.box.x + else: + logger.error( + f"Unknown composition type. " + f"Composition: {composition}. " + f"Paragraph: {paragraph}. " + ) + continue + return length + + +def get_paragraph_unicode(paragraph: PdfParagraph) -> str: + chars = [] + for composition in paragraph.pdf_paragraph_composition: + if composition.pdf_line: + chars.extend(composition.pdf_line.pdf_character) + elif composition.pdf_same_style_characters: + chars.extend(composition.pdf_same_style_characters.pdf_character) + elif composition.pdf_same_style_unicode_characters: + chars.extend(composition.pdf_same_style_unicode_characters.unicode) + elif composition.pdf_formula: + chars.extend(composition.pdf_formula.pdf_character) + elif composition.pdf_character: + chars.append(composition.pdf_character) + else: + logger.error( + f"Unknown composition type. " + f"Composition: {composition}. " + f"Paragraph: {paragraph}. " + ) + continue + return get_char_unicode_string(chars) + + +def get_char_unicode_string(chars: list[PdfCharacter | str]) -> str: + """ + 将字符列表转换为 Unicode 字符串,根据字符间距自动插入空格。 + 有些 PDF 不会显式编码空格,这时需要根据间距自动插入空格。 + + Args: + chars: 字符列表,可以是 PdfCharacter 对象或字符串 + + Returns: + str: 处理后的 Unicode 字符串 + """ + # 计算字符间距的中位数 + distances = [] + for i in range(len(chars) - 1): + if not ( + isinstance(chars[i], PdfCharacter) + and isinstance(chars[i + 1], PdfCharacter) + ): + continue + distance = chars[i + 1].box.x - chars[i].box.x2 + if distance > 1: # 只考虑正向距离 + distances.append(distance) + + # 去重后的距离 + distinct_distances = sorted(set(distances)) + + if not distinct_distances: + median_distance = 1 + elif len(distinct_distances) == 1: + median_distance = distinct_distances[0] + else: + median_distance = distinct_distances[1] + + # 构建 unicode 字符串,根据间距插入空格 + unicode_chars = [] + for i in range(len(chars)): + # 如果不是字符对象,直接添加,一般来说这个时候 chars[i] 是字符串 + if not isinstance(chars[i], PdfCharacter): + unicode_chars.append(chars[i]) + continue + unicode_chars.append(chars[i].char_unicode) + + # 如果是空格,跳过 + if chars[i].char_unicode == " ": + continue + + # 如果两个字符都是 PdfCharacter,检查间距 + if i < len(chars) - 1 and isinstance(chars[i + 1], PdfCharacter): + distance = chars[i + 1].box.x - chars[i].box.x2 + if distance >= median_distance or Layout.is_newline( # 间距大于中位数 + chars[i], chars[i + 1] + ): # 换行 + unicode_chars.append(" ") # 添加空格 + + return "".join(unicode_chars) + + +def get_paragraph_max_height(paragraph: PdfParagraph) -> float: + """ + 获取段落中最高的排版单元高度。 + + Args: + paragraph: PDF段落对象 + + Returns: + float: 最大高度值 + """ + max_height = 0.0 + for composition in paragraph.pdf_paragraph_composition: + if composition is None: + continue + if composition.pdf_character: + char_height = ( + composition.pdf_character[0].box.y2 - composition.pdf_character[0].box.y + ) + max_height = max(max_height, char_height) + elif composition.pdf_same_style_characters: + for pdf_char in composition.pdf_same_style_characters.pdf_character: + char_height = pdf_char.box.y2 - pdf_char.box.y + max_height = max(max_height, char_height) + elif composition.pdf_same_style_unicode_characters: + # 对于纯Unicode字符,我们使用其样式中的字体大小作为高度估计 + font_size = ( + composition.pdf_same_style_unicode_characters.pdf_style.font_size + ) + max_height = max(max_height, font_size) + elif composition.pdf_line: + for pdf_char in composition.pdf_line.pdf_character: + char_height = pdf_char.box.y2 - pdf_char.box.y + max_height = max(max_height, char_height) + elif composition.pdf_formula: + formula_height = ( + composition.pdf_formula.box.y2 - composition.pdf_formula.box.y + ) + max_height = max(max_height, formula_height) + else: + logger.error( + f"Unknown composition type. " + f"Composition: {composition}. " + f"Paragraph: {paragraph}. " + ) + continue + return max_height + + +def is_same_style(style1, style2) -> bool: + """判断两个样式是否相同""" + if style1 is None or style2 is None: + return style1 is style2 + + return ( + style1.font_id == style2.font_id + and math.fabs(style1.font_size - style2.font_size) < 0.02 + and is_same_graphic_state(style1.graphic_state, style2.graphic_state) + ) + + +def is_same_style_except_size(style1, style2) -> bool: + """判断两个样式是否相同""" + if style1 is None or style2 is None: + return style1 is style2 + + return ( + style1.font_id == style2.font_id + and 0.7 < math.fabs(style1.font_size / style2.font_size) < 1.3 + and is_same_graphic_state(style1.graphic_state, style2.graphic_state) + ) + + +def is_same_style_except_font(style1, style2) -> bool: + """判断两个样式是否相同""" + if style1 is None or style2 is None: + return style1 is style2 + + return math.fabs( + style1.font_size - style2.font_size + ) < 0.02 and is_same_graphic_state(style1.graphic_state, style2.graphic_state) + + +def is_same_graphic_state(state1: GraphicState, state2: GraphicState) -> bool: + """判断两个 GraphicState 是否相同""" + if state1 is None or state2 is None: + return state1 is state2 + + return ( + state1.linewidth == state2.linewidth + and state1.dash == state2.dash + and state1.flatness == state2.flatness + and state1.intent == state2.intent + and state1.linecap == state2.linecap + and state1.linejoin == state2.linejoin + and state1.miterlimit == state2.miterlimit + and state1.ncolor == state2.ncolor + and state1.scolor == state2.scolor + and state1.stroking_color_space_name == state2.stroking_color_space_name + and state1.non_stroking_color_space_name == state2.non_stroking_color_space_name + and state1.passthrough_per_char_instruction + == state2.passthrough_per_char_instruction + ) + + +def add_space_dummy_chars(paragraph: PdfParagraph) -> None: + """ + 在PDF段落中添加表示空格的dummy字符。 + 这个函数会直接修改传入的paragraph对象,在需要空格的地方添加dummy字符。 + 同时也会处理不同组成部分之间的空格。 + + Args: + paragraph: 需要处理的PDF段落对象 + """ + # 首先处理每个组成部分内部的空格 + for composition in paragraph.pdf_paragraph_composition: + if composition.pdf_line: + chars = composition.pdf_line.pdf_character + _add_space_dummy_chars_to_list(chars) + elif composition.pdf_same_style_characters: + chars = composition.pdf_same_style_characters.pdf_character + _add_space_dummy_chars_to_list(chars) + elif composition.pdf_same_style_unicode_characters: + # 对于unicode字符,不需要处理。 + # 这种类型只会出现在翻译好的结果中 + continue + elif composition.pdf_formula: + chars = composition.pdf_formula.pdf_character + _add_space_dummy_chars_to_list(chars) + + # 然后处理组成部分之间的空格 + for i in range(len(paragraph.pdf_paragraph_composition) - 1): + curr_comp = paragraph.pdf_paragraph_composition[i] + next_comp = paragraph.pdf_paragraph_composition[i + 1] + + # 获取当前组成部分的最后一个字符 + curr_last_char = _get_last_char_from_composition(curr_comp) + if not curr_last_char: + continue + + # 获取下一个组成部分的第一个字符 + next_first_char = _get_first_char_from_composition(next_comp) + if not next_first_char: + continue + + # 检查两个组成部分之间是否需要添加空格 + distance = next_first_char.box.x - curr_last_char.box.x2 + if distance > 1: # 只考虑正向距离 + # 创建一个dummy字符作为空格 + space_box = Box( + x=curr_last_char.box.x2, + y=curr_last_char.box.y, + x2=curr_last_char.box.x2 + distance, + y2=curr_last_char.box.y2, + ) + + space_char = PdfCharacter( + pdf_style=curr_last_char.pdf_style, + box=space_box, + char_unicode=" ", + scale=curr_last_char.scale, + advance=space_box.x2 - space_box.x, + ) + + # 将空格添加到当前组成部分的末尾 + if curr_comp.pdf_line: + curr_comp.pdf_line.pdf_character.append(space_char) + elif curr_comp.pdf_same_style_characters: + curr_comp.pdf_same_style_characters.pdf_character.append(space_char) + elif curr_comp.pdf_formula: + curr_comp.pdf_formula.pdf_character.append(space_char) + + +def _get_first_char_from_composition( + comp: PdfParagraphComposition, +) -> PdfCharacter | None: + """获取组成部分的第一个字符""" + if comp.pdf_line and comp.pdf_line.pdf_character: + return comp.pdf_line.pdf_character[0] + elif ( + comp.pdf_same_style_characters and comp.pdf_same_style_characters.pdf_character + ): + return comp.pdf_same_style_characters.pdf_character[0] + elif comp.pdf_formula and comp.pdf_formula.pdf_character: + return comp.pdf_formula.pdf_character[0] + elif comp.pdf_character: + return comp.pdf_character + return None + + +def _get_last_char_from_composition( + comp: PdfParagraphComposition, +) -> PdfCharacter | None: + """获取组成部分的最后一个字符""" + if comp.pdf_line and comp.pdf_line.pdf_character: + return comp.pdf_line.pdf_character[-1] + elif ( + comp.pdf_same_style_characters and comp.pdf_same_style_characters.pdf_character + ): + return comp.pdf_same_style_characters.pdf_character[-1] + elif comp.pdf_formula and comp.pdf_formula.pdf_character: + return comp.pdf_formula.pdf_character[-1] + elif comp.pdf_character: + return comp.pdf_character + return None + + +def _add_space_dummy_chars_to_list(chars: list[PdfCharacter]) -> None: + """ + 在字符列表中的适当位置添加表示空格的dummy字符。 + + Args: + chars: PdfCharacter对象列表 + """ + if not chars: + return + + # 计算字符间距的中位数 + distances = [] + for i in range(len(chars) - 1): + distance = chars[i + 1].box.x - chars[i].box.x2 + if distance > 1: # 只考虑正向距离 + distances.append(distance) + + # 去重后的距离 + distinct_distances = sorted(set(distances)) + + if not distinct_distances: + median_distance = 1 + elif len(distinct_distances) == 1: + median_distance = distinct_distances[0] + else: + median_distance = distinct_distances[1] + + # 在需要的地方插入空格字符 + i = 0 + while i < len(chars) - 1: + curr_char = chars[i] + next_char = chars[i + 1] + + distance = next_char.box.x - curr_char.box.x2 + if distance >= median_distance or Layout.is_newline(curr_char, next_char): + # 创建一个dummy字符作为空格 + space_box = Box( + x=curr_char.box.x2, + y=curr_char.box.y, + x2=curr_char.box.x2 + min(distance, median_distance), + y2=curr_char.box.y2, + ) + + space_char = PdfCharacter( + pdf_style=curr_char.pdf_style, + box=space_box, + char_unicode=" ", + scale=curr_char.scale, + advance=space_box.x2 - space_box.x, + ) + + # 在当前位置后插入空格字符 + chars.insert(i + 1, space_char) + i += 2 # 跳过刚插入的空格 + else: + i += 1 diff --git a/src/pdf2u/document_il/utils/style_helper.py b/src/pdf2u/document_il/utils/style_helper.py new file mode 100644 index 0000000000000000000000000000000000000000..5bfd3a109a6606d7407d684c8c664a05eae3c8fb --- /dev/null +++ b/src/pdf2u/document_il/utils/style_helper.py @@ -0,0 +1,86 @@ +from pdf2u.document_il import il_version_1 + + +def create_pdf_style(r, g, b, font_id="china-ss", font_size=6): + """ + Create a PdfStyle object from RGB values. + + Args: + r: Red component in range 0-255 + g: Green component in range 0-255 + b: Blue component in range 0-255 + font_id: Font identifier + font_size: Font size + + Returns: + PdfStyle object with the specified color + """ + r, g, b = [x / 255.0 for x in (r, g, b)] + return il_version_1.PdfStyle( + font_id=font_id, + font_size=font_size, + graphic_state=il_version_1.GraphicState( + passthrough_per_char_instruction=f"{r:.10f} {g:.10f} {b:.10f} rg" + ), + ) + + +# Generate all color styles +RED = il_version_1.GraphicState( + passthrough_per_char_instruction="1.0000000000 0.2313725490 0.1882352941 rg " + "1.0000000000 0.2313725490 0.1882352941 RG" +) + +ORANGE = il_version_1.GraphicState( + passthrough_per_char_instruction="1.0000000000 0.5843137255 0.0000000000 rg " + "1.0000000000 0.5843137255 0.0000000000 RG" +) +YELLOW = il_version_1.GraphicState( + passthrough_per_char_instruction="1.0000000000 0.8000000000 0.0000000000 rg " + "1.0000000000 0.8000000000 0.0000000000 RG" +) + +GREEN = il_version_1.GraphicState( + passthrough_per_char_instruction="0.2039215686 0.7803921569 0.3490196078 rg " + "0.2039215686 0.7803921569 0.3490196078 RG" +) + +MINT = il_version_1.GraphicState( + passthrough_per_char_instruction="0.0000000000 0.7803921569 0.7450980392 rg " + "0.0000000000 0.7803921569 0.7450980392 RG" +) + +TEAL = il_version_1.GraphicState( + passthrough_per_char_instruction="0.1882352941 0.6901960784 0.7803921569 rg " + "0.1882352941 0.6901960784 0.7803921569 RG" +) + +CYAN = il_version_1.GraphicState( + passthrough_per_char_instruction="0.1960784314 0.6784313725 0.9019607843 rg " + "0.1960784314 0.6784313725 0.9019607843 RG" +) + +BLUE = il_version_1.GraphicState( + passthrough_per_char_instruction="0.0000000000 0.4784313725 1.0000000000 rg " + "0.0000000000 0.4784313725 1.0000000000 RG" +) + +INDIGO = il_version_1.GraphicState( + passthrough_per_char_instruction="0.3450980392 0.3372549020 0.8392156863 rg " + "0.3450980392 0.3372549020 0.8392156863 RG" +) + +PURPLE = il_version_1.GraphicState( + passthrough_per_char_instruction="0.6862745098 0.3215686275 0.8705882353 rg " + "0.6862745098 0.3215686275 0.8705882353 RG" +) + +PINK = il_version_1.GraphicState( + passthrough_per_char_instruction="1.0000000000 0.1764705882 0.3333333333 rg " + "1.0000000000 0.1764705882 0.3333333333 RG" +) + +BROWN = il_version_1.GraphicState( + passthrough_per_char_instruction="0.6352941176 0.5176470588 0.3686274510 rg " + "0.6352941176 0.5176470588 0.3686274510 RG" +) diff --git a/src/pdf2u/document_il/xml_converter.py b/src/pdf2u/document_il/xml_converter.py new file mode 100644 index 0000000000000000000000000000000000000000..9155d928c4bfa22e7aa078d0910d5d58a5b3983e --- /dev/null +++ b/src/pdf2u/document_il/xml_converter.py @@ -0,0 +1,48 @@ +import copy +from pathlib import Path + +import orjson +from xsdata.formats.dataclass.context import XmlContext +from xsdata.formats.dataclass.parsers import XmlParser +from xsdata.formats.dataclass.serializers import XmlSerializer +from xsdata.formats.dataclass.serializers.config import SerializerConfig + +from pdf2u.document_il import il_version_1 + + +class XMLConverter: + def __init__(self): + self.parser = XmlParser() + config = SerializerConfig(indent=" ") + context = XmlContext() + self.serializer = XmlSerializer(context=context, config=config) + + def write_xml(self, document: il_version_1.Document, path: str): + with Path(path).open("w", encoding="utf-8") as f: + f.write(self.to_xml(document)) + + def read_xml(self, path: str) -> il_version_1.Document: + with Path(path).open(encoding="utf-8") as f: + return self.from_xml(f.read()) + + def to_xml(self, document: il_version_1.Document) -> str: + return self.serializer.render(document) + + def from_xml(self, xml: str) -> il_version_1.Document: + return self.parser.from_string(xml, il_version_1.Document) + + def deepcopy(self, document: il_version_1.Document) -> il_version_1.Document: + return copy.deepcopy(document) + # return self.from_xml(self.to_xml(document)) + + def to_json(self, document: il_version_1.Document) -> str: + return orjson.dumps( + document, + option=orjson.OPT_APPEND_NEWLINE + | orjson.OPT_INDENT_2 + | orjson.OPT_SORT_KEYS, + ).decode() + + def write_json(self, document: il_version_1.Document, path: str): + with Path(path).open("w", encoding="utf-8") as f: + f.write(self.to_json(document)) diff --git a/src/pdf2u/docvision/README.md b/src/pdf2u/docvision/README.md new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/pdf2u/docvision/__init__.py b/src/pdf2u/docvision/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/pdf2u/docvision/__pycache__/__init__.cpython-311.pyc b/src/pdf2u/docvision/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0e97bdbb2be85c0c3f6ea0012948e4c5f3944a9f Binary files /dev/null and b/src/pdf2u/docvision/__pycache__/__init__.cpython-311.pyc differ diff --git a/src/pdf2u/docvision/__pycache__/__init__.cpython-312.pyc b/src/pdf2u/docvision/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e1746dc8e144918c141f31db36f12773d4f9af54 Binary files /dev/null and b/src/pdf2u/docvision/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/pdf2u/docvision/__pycache__/doclayout.cpython-311.pyc b/src/pdf2u/docvision/__pycache__/doclayout.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f71f68d31a6112a5a1cc28fa622bdaa4f4c9e218 Binary files /dev/null and b/src/pdf2u/docvision/__pycache__/doclayout.cpython-311.pyc differ diff --git a/src/pdf2u/docvision/__pycache__/doclayout.cpython-312.pyc b/src/pdf2u/docvision/__pycache__/doclayout.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..21babcd0ce2084c55f74511617cb3882b9d7bb08 Binary files /dev/null and b/src/pdf2u/docvision/__pycache__/doclayout.cpython-312.pyc differ diff --git a/src/pdf2u/docvision/doclayout.py b/src/pdf2u/docvision/doclayout.py new file mode 100644 index 0000000000000000000000000000000000000000..b41293557d34a3795d3910dfcedbd17f9c36e9e2 --- /dev/null +++ b/src/pdf2u/docvision/doclayout.py @@ -0,0 +1,265 @@ +import abc +import ast +import os.path +import platform +from pathlib import Path + +import cv2 +import numpy as np +import onnx +import onnxruntime +from huggingface_hub import hf_hub_download + + +class DocLayoutModel(abc.ABC): + @staticmethod + def load_onnx() -> "OnnxModel": + model = OnnxModel.from_pretrained( + repo_id="wybxc/DocLayout-YOLO-DocStructBench-onnx", + filename="doclayout_yolo_docstructbench_imgsz1024.onnx", + ) + return model + + @staticmethod + def load_available(): + return DocLayoutModel.load_onnx() + + @property + @abc.abstractmethod + def stride(self) -> int: + """Stride of the model input.""" + pass + + @abc.abstractmethod + def predict(self, image, imgsz=1024, **kwargs) -> list: + """ + Predict the layout of a document page. + + Args: + image: The image of the document page. + imgsz: Resize the image to this size. Must be a multiple of the stride. + **kwargs: Additional arguments. + """ + pass + + +class YoloResult: + """Helper class to store detection results from ONNX model.""" + + def __init__(self, names, boxes=None, boxes_data=None): + if boxes is not None: + self.boxes = boxes + else: + assert boxes_data is not None + self.boxes = [YoloBox(data=d) for d in boxes_data] + self.boxes.sort(key=lambda x: x.conf, reverse=True) + self.names = names + + +class YoloBox: + """Helper class to store detection results from ONNX model.""" + + def __init__(self, data=None, xyxy=None, conf=None, cls=None): + if data is not None: + self.xyxy = data[:4] + self.conf = data[-2] + self.cls = data[-1] + return + assert xyxy is not None and conf is not None and cls is not None + self.xyxy = xyxy + self.conf = conf + self.cls = cls + + +# 检测操作系统类型 +os_name = platform.system() + +providers = [] + +if os_name == "Darwin" and False: # Temporarily disable CoreML due to some issues + providers.append( + ( + "CoreMLExecutionProvider", + { + "ModelFormat": "MLProgram", + "MLComputeUnits": "ALL", + "RequireStaticInputShapes": "0", + "EnableOnSubgraphs": "0", + }, + ) + ) + # workaround for CoreML batch inference issues + max_batch_size = 1 +else: + max_batch_size = 1 +providers.append("CPUExecutionProvider") # CPU执行提供者作为通用后备选项 + + +class OnnxModel(DocLayoutModel): + def __init__(self, model_path: str): + self.model_path = model_path + + model = onnx.load(model_path) + metadata = {d.key: d.value for d in model.metadata_props} + self._stride = ast.literal_eval(metadata["stride"]) + self._names = ast.literal_eval(metadata["names"]) + + self.model = onnxruntime.InferenceSession( + model.SerializeToString(), providers=providers + ) + + @staticmethod + def from_pretrained(repo_id: str, filename: str): + if os.environ.get("USE_MODELSCOPE", "0") == "1": + repo_mapping = { + # Edit here to add more models + "wybxc/DocLayout-YOLO-DocStructBench-onnx": "AI-ModelScope/DocLayout-YOLO-DocStructBench-onnx" + } + from modelscope import snapshot_download + + model_dir = snapshot_download(repo_mapping[repo_id]) + pth = Path(model_dir) / filename + else: + try: + pth = hf_hub_download( + repo_id=repo_id, + filename=filename, + etag_timeout=1, + local_files_only=True, + ) + except Exception: + pth = hf_hub_download( + repo_id=repo_id, filename=filename, etag_timeout=1 + ) + return OnnxModel(pth) + + @property + def stride(self): + return self._stride + + def resize_and_pad_image(self, image, new_shape): + """ + Resize and pad the image to the specified size, ensuring dimensions are multiples of stride. + + Parameters: + - image: Input image + - new_shape: Target size (integer or (height, width) tuple) + - stride: Padding alignment stride, default 32 + + Returns: + - Processed image + """ + if isinstance(new_shape, int): + new_shape = (new_shape, new_shape) + + h, w = image.shape[:2] + new_h, new_w = new_shape + + # Calculate scaling ratio + r = min(new_h / h, new_w / w) + resized_h, resized_w = int(round(h * r)), int(round(w * r)) + + # Resize image + image = cv2.resize( + image, (resized_w, resized_h), interpolation=cv2.INTER_LINEAR + ) + + # Calculate padding size and align to stride multiple + pad_w = (new_w - resized_w) % self.stride + pad_h = (new_h - resized_h) % self.stride + top, bottom = pad_h // 2, pad_h - pad_h // 2 + left, right = pad_w // 2, pad_w - pad_w // 2 + + # Add padding + image = cv2.copyMakeBorder( + image, top, bottom, left, right, cv2.BORDER_CONSTANT, value=(114, 114, 114) + ) + + return image + + def scale_boxes(self, img1_shape, boxes, img0_shape): + """ + Rescales bounding boxes (in the format of xyxy by default) from the shape of the image they were originally + specified in (img1_shape) to the shape of a different image (img0_shape). + + Args: + img1_shape (tuple): The shape of the image that the bounding boxes are for, + in the format of (height, width). + boxes (torch.Tensor): the bounding boxes of the objects in the image, in the format of (x1, y1, x2, y2) + img0_shape (tuple): the shape of the target image, in the format of (height, width). + + Returns: + boxes (torch.Tensor): The scaled bounding boxes, in the format of (x1, y1, x2, y2) + """ + + # Calculate scaling ratio + gain = min(img1_shape[0] / img0_shape[0], img1_shape[1] / img0_shape[1]) + + # Calculate padding size + pad_x = round((img1_shape[1] - img0_shape[1] * gain) / 2 - 0.1) + pad_y = round((img1_shape[0] - img0_shape[0] * gain) / 2 - 0.1) + + # Remove padding and scale boxes + boxes[..., :4] = (boxes[..., :4] - [pad_x, pad_y, pad_x, pad_y]) / gain + return boxes + + def predict(self, image, imgsz=1024, batch_size=16, **kwargs): + """ + Predict the layout of document pages. + + Args: + image: A single image or a list of images of document pages. + imgsz: Resize the image to this size. Must be a multiple of the stride. + batch_size: Number of images to process in one batch. + **kwargs: Additional arguments. + + Returns: + A list of YoloResult objects, one for each input image. + """ + # Handle single image input + if isinstance(image, np.ndarray) and len(image.shape) == 3: + image = [image] + + total_images = len(image) + results = [] + batch_size = min(batch_size, max_batch_size) + + # Process images in batches + for i in range(0, total_images, batch_size): + batch_images = image[i : i + batch_size] + batch_size_actual = len(batch_images) + + # Calculate target size based on the maximum height in the batch + max_height = max(img.shape[0] for img in batch_images) + target_imgsz = int(max_height / 32) * 32 + + # Preprocess batch + processed_batch = [] + orig_shapes = [] + for img in batch_images: + orig_h, orig_w = img.shape[:2] + orig_shapes.append((orig_h, orig_w)) + + pix = self.resize_and_pad_image(img, new_shape=target_imgsz) + pix = np.transpose(pix, (2, 0, 1)) # CHW + pix = pix.astype(np.float32) / 255.0 # Normalize to [0, 1] + processed_batch.append(pix) + + # Stack batch + batch_input = np.stack(processed_batch, axis=0) # BCHW + new_h, new_w = batch_input.shape[2:] + + # Run inference + batch_preds = self.model.run(None, {"images": batch_input})[0] + + # Process each prediction in the batch + for j in range(batch_size_actual): + preds = batch_preds[j] + preds = preds[preds[..., 4] > 0.25] + if len(preds) > 0: + preds[..., :4] = self.scale_boxes( + (new_h, new_w), preds[..., :4], orig_shapes[j] + ) + results.append(YoloResult(boxes_data=preds, names=self._names)) + + return results diff --git a/src/pdf2u/docvision/rpc_doclayout.py b/src/pdf2u/docvision/rpc_doclayout.py new file mode 100644 index 0000000000000000000000000000000000000000..936b5419e651efff42ccee2878abd284cbe7cf60 --- /dev/null +++ b/src/pdf2u/docvision/rpc_doclayout.py @@ -0,0 +1,153 @@ +import logging +from pathlib import Path + +import cv2 +import httpx +import msgpack +import numpy as np + +from pdf2u.docvision.doclayout import DocLayoutModel, YoloBox, YoloResult + +# 设置日志 +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger(__name__) + + +def encode_image(image) -> bytes: + """Read and encode image to bytes + + Args: + image: Can be either a file path (str) or numpy array + """ + if isinstance(image, str): + if not Path(image).exists(): + raise FileNotFoundError(f"Image file not found: {image}") + img = cv2.imread(image) + if img is None: + raise ValueError(f"Failed to read image: {image}") + else: + img = image + + # logger.debug(f"Image shape: {img.shape}") + encoded = cv2.imencode(".jpg", img)[1].tobytes() + # logger.debug(f"Encoded image size: {len(encoded)} bytes") + return encoded + + +def predict_layout(image, host: str = "http://localhost:8000", imgsz: int = 1024): + """ + Predict document layout using the MOSEC service + + Args: + image: Can be either a file path (str) or numpy array + host: Service host URL + imgsz: Image size for model input + + Returns: + List of predictions containing bounding boxes and classes + """ + try: + # Prepare request data + if not isinstance(image, list): + image = [image] + image_data = [encode_image(image) for image in image] + data = {"image": image_data, "imgsz": imgsz} + + # Pack data using msgpack + packed_data = msgpack.packb(data, use_bin_type=True) + logger.debug(f"Packed data size: {len(packed_data)} bytes") + + # Send request + logger.debug(f"Sending request to {host}/inference") + response = httpx.post( + f"{host}/inference", + data=packed_data, + headers={ + "Content-Type": "application/msgpack", + "Accept": "application/msgpack", + }, + timeout=300, + follow_redirects=True, + ) + + logger.debug(f"Response status: {response.status_code}") + logger.debug(f"Response headers: {response.headers}") + + if response.status_code == 200: + try: + result = msgpack.unpackb(response.content, raw=False) + return result + except Exception as e: + logger.exception(f"Failed to unpack response: {e!s}") + raise + else: + logger.error(f"Request failed with status {response.status_code}") + logger.error(f"Response content: {response.content}") + raise Exception( + f"Request failed with status {response.status_code}: {response.text}" + ) + except Exception as e: + logger.exception(f"Unexpected error: {e!s}") + raise + + +class RpcDocLayoutModel(DocLayoutModel): + """DocLayoutModel implementation that uses RPC service.""" + + def __init__(self, host: str = "http://localhost:8000"): + """Initialize RPC model with host address.""" + self.host = host + self._stride = 32 # Default stride value + self._names = ["text", "title", "list", "table", "figure"] + + @property + def stride(self) -> int: + """Stride of the model input.""" + return self._stride + + def predict(self, image, imgsz=1024, **kwargs) -> list[YoloResult]: + """Predict the layout of document pages using RPC service.""" + # Handle single image input + if isinstance(image, np.ndarray) and len(image.shape) == 3: + image = [image] + + results = [] + preds = predict_layout(image, host=self.host, imgsz=imgsz) + if len(preds) > 0: + for pred in preds: + boxes = [ + YoloBox(None, np.array(x["xyxy"]), np.array(x["conf"]), x["cls"]) + for x in pred["boxes"] + ] + results.append( + YoloResult( + boxes=boxes, names={int(k): v for k, v in pred["names"].items()} + ) + ) + else: + results.append(YoloResult(boxes_data=np.array([]), names=[])) + + return results + + @staticmethod + def from_host(host: str) -> "RpcDocLayoutModel": + """Create RpcDocLayoutModel from host address.""" + return RpcDocLayoutModel(host=host) + + +if __name__ == "__main__": + # Test the service + try: + # Use a default test image if example/1.png doesn't exist + image_path = "example/1.png" + if not Path(image_path).exists(): + print(f"Warning: {image_path} not found.") + print("Please provide the path to a test image:") + image_path = input("> ") + + logger.info(f"Processing image: {image_path}") + result = predict_layout(image_path) + print("Prediction results:") + print(result) + except Exception as e: + print(f"Error: {e!s}") diff --git a/src/pdf2u/gui.py b/src/pdf2u/gui.py new file mode 100644 index 0000000000000000000000000000000000000000..4a14b4cffbf12fa7a0e1773957dc7217083b40de --- /dev/null +++ b/src/pdf2u/gui.py @@ -0,0 +1,380 @@ +import asyncio +import base64 +import tempfile +from dataclasses import asdict, dataclass +from pathlib import Path + +import streamlit as st +from streamlit.runtime.uploaded_file_manager import UploadedFile + +import pdf2u.high_level +from pdf2u.const import TranslationService, get_cache_file_path +from pdf2u.document_il.translator.translator import ( + BaseTranslator, + BingTranslator, + GoogleTranslator, + OpenAITranslator, + set_translate_rate_limiter, +) +from pdf2u.docvision.doclayout import DocLayoutModel, OnnxModel +from pdf2u.translation_config import TranslationConfig + + +def create_pdf_html(base64_pdf: str) -> str: + """Custom HTML/CSS for PDF display with page number""" + pdf_display = f""" +
+ +
+ """ + return pdf_display + + +@st.cache_resource +def load_onnx_model() -> OnnxModel: + return DocLayoutModel.load_onnx() + + +@st.cache_resource +def init_backend() -> None: + pdf2u.high_level.init() + + +async def process_file(config: TranslationConfig, debug: bool) -> None: + """Process a single PDF file.""" + async for event in pdf2u.high_level.async_translate(config): + if event["type"] == "finish": + result = event["translate_result"] + return result + + +def translate_pdf( + input_file: Path, + output_dir: Path, + service: TranslationService = TranslationService.GOOGLE, + openai_api_key: str | None = None, + openai_model: str | None = None, + openai_base_url: str | None = None, + source_lang: str = "en", + target_lang: str = "zh", + qps: int = 10, + ignore_cache: bool = False, +) -> tuple[Path, Path] | None: + """Translate PDF files using various translation services.""" + + # Validate OpenAI settings + if service == TranslationService.OPENAI and not openai_api_key: + st.toast("Error: Must specify OpenAI API key (--openai-api-key)") + return + + # Initialize translator + translator: BaseTranslator + if service == TranslationService.OPENAI: + translator = OpenAITranslator( + lang_in=source_lang, + lang_out=target_lang, + model=openai_model, + base_url=openai_base_url, + api_key=openai_api_key, + ignore_cache=ignore_cache, + ) + elif service == TranslationService.BING: + translator = BingTranslator( + lang_in=source_lang, lang_out=target_lang, ignore_cache=ignore_cache + ) + else: # Google + translator = GoogleTranslator( + lang_in=source_lang, lang_out=target_lang, ignore_cache=ignore_cache + ) + + # Set translation rate limit + set_translate_rate_limiter(qps) + + # Initialize document layout model + doc_layout_model = load_onnx_model() + + # Get and validate font + font_path = get_cache_file_path("source-han-serif-cn.ttf") + if font_path: + if not Path(font_path).exists(): + st.toast(f"Error: Font file not found: {font_path}") + return + if not str(font_path).endswith(".ttf"): + st.toast(f"Error: Invalid font file: {font_path}") + return + + config = TranslationConfig( + input_file=str(input_file), + font=font_path.as_posix() if font_path else None, + output_dir=str(output_dir) if output_dir else None, + translator=translator, + debug=False, + lang_in=source_lang, + lang_out=target_lang, + no_dual=False, + no_mono=False, + qps=qps, + doc_layout_model=doc_layout_model, + skip_clean=False, + enhance_compatibility=False, + ) + # Execute + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + # 运行异步函数 + result = loop.run_until_complete(process_file(config, debug=False)) + translated_pdf = Path(result.mono_pdf_path) if result.mono_pdf_path else None + bilingual_pdf = Path(result.dual_pdf_path) if result.dual_pdf_path else None + + if not translated_pdf or not bilingual_pdf: + st.toast("Error: Translation failed") + return + + return translated_pdf, bilingual_pdf + finally: + loop.close() + + +def reset_file_state() -> None: + st.session_state.pdf_state = PDFState(original_pdf=st.session_state.uploaded_file) + + +def start_translate() -> None: + """Translate Config InterFace""" + st.session_state.pdf_state.start_translate = True + st.toast(body="Translation started! 🎉") + init_backend() + # update config with session state + for k, v in st.session_state.items(): + if k in asdict(st.session_state.config): + st.session_state.config.__setattr__(k, v) + config = st.session_state.config + with tempfile.TemporaryDirectory() as tmp_dir: + tmp_output_dir = Path(tmp_dir) + with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp_file: + tmp_file.write( + get_pdf_content(st.session_state.pdf_state.original_pdf, s_b=False) + ) + tmp_input_file = Path(tmp_file.name) + + try: + # invoke translation + translated_pdf, bilingual_pdf = translate_pdf( + input_file=tmp_input_file, + output_dir=tmp_output_dir, + service=config.service, + openai_api_key=config.api_key + if config.service == TranslationService.OPENAI + else None, + source_lang=config.source_lang, + target_lang=config.target_lang, + qps=config.qps, + ignore_cache=config.ignore_cache, + ) + with open(translated_pdf, "rb") as f: + translated_pdf_content = f.read() + with open(bilingual_pdf, "rb") as f: + bilingual_pdf_content = f.read() + + # update session state + st.session_state.pdf_state.translated_pdf = translated_pdf_content + st.session_state.pdf_state.bilingual_pdf = bilingual_pdf_content + st.session_state.pdf_state.translated = True + st.session_state.pdf_state.start_translate = False + + st.toast("Translation completed! 🎉") + except Exception as e: + st.error(f"Translation failed: {str(e)}") + finally: + tmp_input_file.unlink() + + # # Test + # translated_pdf_path = Path(__file__).parents[2] / "tests" / "data" / "sample.pdf" + # st.session_state.pdf_state.translated_pdf = translated_pdf_path + # st.session_state.pdf_state.bilingual_pdf = translated_pdf_path + + # st.session_state.pdf_state.translated = True + # st.session_state.pdf_state.start_translate = False + + # st.toast(st.session_state.config) + # st.toast(st.session_state.pdf_state) + + +def show_config_sidebar() -> None: + with st.sidebar: + st.title("PDF Translator") + + st.file_uploader( + " ", type="pdf", key="uploaded_file", on_change=reset_file_state + ) + + if st.session_state.pdf_state.translated: + create_download_buttons( + st.session_state.pdf_state.translated_pdf, + st.session_state.pdf_state.bilingual_pdf, + ) + + if st.session_state.pdf_state.original_pdf is not None: + st.button( + "🚀 Translate", + type="primary", + use_container_width=True, + on_click=start_translate, + ) + else: + st.info("Please upload a PDF file to translate.") + + # st.divider() + st.selectbox( + "Service 🔄", + options=["google", "openai", "bing"], + key="service", + help="select translation service", + ) + + if st.session_state.service == TranslationService.OPENAI: + st.text_input( + "OpenAI API Key 🔑", + type="password", + key="api_key", + help="get your API key from OpenAI", + ) + + col1, col2 = st.columns(2) + with col1: + st.selectbox( + "Origin 📖", + options=["en", "zh", "ja", "ko", "fr", "de"], + key="source_lang", + ) + with col2: + st.selectbox( + "Target 📝", + options=["zh", "en", "ja", "ko", "fr", "de"], + key="target_lang", + ) + + st.number_input( + "QPS limit ⚡", + min_value=1, + value=10, + key="qps", + help="queries per second limit", + ) + + col3, col4 = st.columns(2) + with col3: + st.toggle("No Cache 🚫", key="ignore_cache") + + +def create_download_buttons(translated_pdf: bytes, bilingual_pdf: bytes) -> None: + col1, col2 = st.columns(2) + with col1: + st.download_button( + label="📥 Download Translation Only", + data=translated_pdf, + file_name="translated.pdf", + mime="application/pdf", + ) + with col2: + st.download_button( + label="📥 Download Bilingual", + data=bilingual_pdf, + file_name="bilingual.pdf", + mime="application/pdf", + ) + + +@dataclass +class Config: + service: TranslationService = TranslationService.GOOGLE + api_key: str = "sk-1234567890" + source_lang: str = "en" + target_lang: str = "zh" + qps: int = 10 + ignore_cache: bool = False + + +@dataclass +class PDFState: + translated: bool = False + start_translate: bool = False + original_pdf: UploadedFile = None + translated_pdf: bytes = None + bilingual_pdf: bytes = None + + +def init_session_state() -> None: + """Initialize session state variables.""" + if "config" not in st.session_state: + # translate config + st.session_state.config = Config() + + if "pdf_state" not in st.session_state: + st.session_state.pdf_state = PDFState() + + +@st.cache_data +def get_pdf_content( + uploaded_file: UploadedFile | Path | bytes, s_b: bool = True +) -> str: + if isinstance(uploaded_file, Path): + with open(uploaded_file, "rb") as f: + pdf_content = f.read() + elif isinstance(uploaded_file, bytes): + pdf_content = uploaded_file + else: + uploaded_file.seek(0) # Reset file pointer + pdf_content = uploaded_file.read() + if s_b: + return base64.b64encode(pdf_content).decode("utf-8") + else: + return pdf_content + + +def main() -> None: + st.set_page_config( + layout="wide", page_title="PDF Translator", initial_sidebar_state="expanded" + ) + + init_session_state() + show_config_sidebar() + + if st.session_state.pdf_state.start_translate: + with st.spinner("Translating..."): + pass + if st.session_state.pdf_state.original_pdf is not None: + # st.toast(f"PDF uploaded! 📄{st.session_state.pdf_state.original_pdf.name}") + # Display PDF + if st.session_state.pdf_state.translated: + st.markdown("#### 📝 Bilingual PDF") + pdf_display_base64 = get_pdf_content( + st.session_state.pdf_state.bilingual_pdf + ) + else: + st.markdown("#### 📄 Original PDF") + pdf_display_base64 = get_pdf_content( + st.session_state.pdf_state.original_pdf + ) + pdf_display = create_pdf_html(pdf_display_base64) + st.markdown(pdf_display, unsafe_allow_html=True) + else: + st.markdown( + """ +
+

👈 Please Upload PDF

+
+ """, + unsafe_allow_html=True, + ) + + +if __name__ == "__main__": + main() diff --git a/src/pdf2u/high_level.py b/src/pdf2u/high_level.py new file mode 100644 index 0000000000000000000000000000000000000000..9dc12c7afd1a334941bd6da0099c82c8c20e04ba --- /dev/null +++ b/src/pdf2u/high_level.py @@ -0,0 +1,461 @@ +import asyncio +import hashlib +import logging +import threading +import time +from asyncio import CancelledError +from pathlib import Path +from typing import Any, BinaryIO + +import httpx +from pdfminer.pdfdocument import PDFDocument +from pdfminer.pdfinterp import PDFResourceManager +from pdfminer.pdfpage import PDFPage +from pdfminer.pdfparser import PDFParser +from pymupdf import Document, Font + +from pdf2u import asynchronize +from pdf2u.const import CACHE_FOLDER, get_cache_file_path +from pdf2u.converter import TranslateConverter +from pdf2u.document_il.backend.pdf_creater import ( + SAVE_PDF_STAGE_NAME, + SUBSET_FONT_STAGE_NAME, + PDFCreater, +) +from pdf2u.document_il.frontend.il_creater import ILCreater +from pdf2u.document_il.midend.add_debug_information import AddDebugInformation +from pdf2u.document_il.midend.il_translator import ILTranslator +from pdf2u.document_il.midend.layout_parser import LayoutParser +from pdf2u.document_il.midend.paragraph_finder import ParagraphFinder +from pdf2u.document_il.midend.remove_descent import RemoveDescent +from pdf2u.document_il.midend.styles_and_formulas import StylesAndFormulas +from pdf2u.document_il.midend.typesetting import Typesetting +from pdf2u.document_il.utils.fontmap import FontMapper +from pdf2u.document_il.xml_converter import XMLConverter +from pdf2u.pdfinterp import PDFPageInterpreterEx +from pdf2u.progress_monitor import ProgressMonitor +from pdf2u.translation_config import TranslateResult, TranslationConfig + +logger = logging.getLogger(__name__) + +TRANSLATE_STAGES = [ + ILCreater.stage_name, + LayoutParser.stage_name, + ParagraphFinder.stage_name, + StylesAndFormulas.stage_name, + RemoveDescent.stage_name, + ILTranslator.stage_name, + Typesetting.stage_name, + FontMapper.stage_name, + PDFCreater.stage_name, + SUBSET_FONT_STAGE_NAME, + SAVE_PDF_STAGE_NAME, +] + +resfont_map = { + "zh-cn": "china-ss", + "zh-tw": "china-ts", + "zh-hans": "china-ss", + "zh-hant": "china-ts", + "zh": "china-ss", + "ja": "japan-s", + "ko": "korea-s", +} + +FONT_ASSETS = [ + ( + "noto.ttf", + "https://github.com/satbyy/go-noto-universal" + "/releases/download/v7.0/GoNotoKurrent-Regular.ttf", + "2f2cee5fbb2403df352ca2005247f6c4faa70f3086ebd31b6c62308b5f2f9865", + ), + ( + "source-han-serif-cn.ttf", + "https://github.com/junmer/source-han-serif-ttf/" + "raw/refs/heads/master/SubsetTTF/CN/SourceHanSerifCN-Regular.ttf", + "1e60cc2eedfa25bf5e4ecaa794402f581ad770d4c8be46d338bf52064b307ec7", + ), + ( + "source-han-serif-cn-bold.ttf", + "https://github.com/junmer/source-han-serif-ttf/" + "raw/refs/heads/master/SubsetTTF/CN/SourceHanSerifCN-Bold.ttf", + "84c24723a47537fcf5057b788a51c41978ee6173931f19b8a9f5a4595b677dc9", + ), + ( + "SourceHanSansSC-Regular.ttf", + "https://github.com/iizyd/SourceHanSansCN-TTF-Min/" + "raw/refs/heads/main/source-file/ttf/SourceHanSansSC-Regular.ttf", + "a878f16eed162dc5b211d888a4a29b1730b73f4cf632e720abca4eab7bd8a152", + ), + ( + "SourceHanSansSC-Bold.ttf", + "https://github.com/iizyd/SourceHanSansCN-TTF-Min/" + "raw/refs/heads/main/source-file/ttf/SourceHanSansSC-Bold.ttf", + "485b27eb4f3603223e9c3c5ebfa317aee77772ea8f642f9330df7f030c8b7b43", + ), + ( + "LXGWWenKai-Regular.ttf", + "https://github.com/lxgw/LxgwWenKai/" + "raw/refs/heads/main/fonts/TTF/LXGWWenKai-Regular.ttf", + "ea47ec17d0f3d0ed1e6d9c51d6146402d4d1e2f0ff397a90765aaaa0ddd382fb", + ), +] + + +def verify_file_hash(file_path: str, expected_hash: str) -> bool: + """Verify the SHA256 hash of a file.""" + sha256_hash = hashlib.sha256() + with Path(file_path).open("rb") as f: + # Read the file in chunks to handle large files efficiently + for byte_block in iter(lambda: f.read(4096), b""): + sha256_hash.update(byte_block) + return sha256_hash.hexdigest() == expected_hash + + +def start_parse_il( + inf: BinaryIO, + pages: list[int] | None = None, + vfont: str = "", + vchar: str = "", + thread: int = 0, + doc_zh: Document = None, + lang_in: str = "", + lang_out: str = "", + service: str = "", + resfont: str = "", + noto: Font = None, + cancellation_event: asyncio.Event = None, + il_creater: ILCreater = None, + translation_config: TranslationConfig = None, + **kwarg: Any, +) -> None: + rsrcmgr = PDFResourceManager() + layout = {} + device = TranslateConverter( + rsrcmgr, + vfont, + vchar, + thread, + layout, + lang_in, + lang_out, + service, + resfont, + noto, + kwarg.get("envs", {}), + kwarg.get("prompt", []), + il_creater=il_creater, + ) + # model = DocLayoutModel.load_available() + + assert device is not None + assert il_creater is not None + assert translation_config is not None + obj_patch = {} + interpreter = PDFPageInterpreterEx(rsrcmgr, device, obj_patch, il_creater) + if pages: + total_pages = len(pages) + else: + total_pages = doc_zh.page_count + + il_creater.on_total_pages(total_pages) + + parser = PDFParser(inf) + doc = PDFDocument(parser) + + for pageno, page in enumerate(PDFPage.create_pages(doc)): + if cancellation_event and cancellation_event.is_set(): + raise CancelledError("task cancelled") + if pages and (pageno not in pages): + continue + page.pageno = pageno + if not translation_config.should_translate_page(pageno + 1): + continue + translation_config.raise_if_cancelled() + # The current program no longer relies on + # the following layout recognition results, + # but in order to facilitate the migration of pdf2zh, + # the relevant code is temporarily retained. + # pix = doc_zh[page.pageno].get_pixmap() + # image = np.fromstring(pix.samples, np.uint8).reshape( + # pix.height, pix.width, 3 + # )[:, :, ::-1] + # page_layout = model.predict( + # image, imgsz=int(pix.height / 32) * 32)[0] + # # kdtree 是不可能 kdtree 的,不如直接渲染成图片,用空间换时间 + # box = np.ones((pix.height, pix.width)) + # h, w = box.shape + # vcls = ["abandon", "figure", "table", + # "isolate_formula", "formula_caption"] + # for i, d in enumerate(page_layout.boxes): + # if page_layout.names[int(d.cls)] not in vcls: + # x0, y0, x1, y1 = d.xyxy.squeeze() + # x0, y0, x1, y1 = ( + # np.clip(int(x0 - 1), 0, w - 1), + # np.clip(int(h - y1 - 1), 0, h - 1), + # np.clip(int(x1 + 1), 0, w - 1), + # np.clip(int(h - y0 + 1), 0, h - 1), + # ) + # box[y0:y1, x0:x1] = i + 2 + # for i, d in enumerate(page_layout.boxes): + # if page_layout.names[int(d.cls)] in vcls: + # x0, y0, x1, y1 = d.xyxy.squeeze() + # x0, y0, x1, y1 = ( + # np.clip(int(x0 - 1), 0, w - 1), + # np.clip(int(h - y1 - 1), 0, h - 1), + # np.clip(int(x1 + 1), 0, w - 1), + # np.clip(int(h - y0 + 1), 0, h - 1), + # ) + # box[y0:y1, x0:x1] = 0 + # layout[page.pageno] = box + # 新建一个 xref 存放新指令流 + page.page_xref = doc_zh.get_new_xref() # hack 插入页面的新 xref + doc_zh.update_object(page.page_xref, "<<>>") + doc_zh.update_stream(page.page_xref, b"") + doc_zh[page.pageno].set_contents(page.page_xref) + ops_base = interpreter.process_page(page) + il_creater.on_page_base_operation(ops_base) + il_creater.on_page_end() + il_creater.on_finish() + device.close() + + +def translate(translation_config: TranslationConfig) -> TranslateResult: + with ProgressMonitor(translation_config, TRANSLATE_STAGES) as pm: + return do_translate(pm, translation_config) + + +async def async_translate(translation_config: TranslationConfig): + """Asynchronously translate a PDF file with real-time progress reporting. + + This function yields progress events that can be used to update progress bars + or other UI elements. The events are dictionaries with the following structure: + + - progress_start: { + "type": "progress_start", + "stage": str, # Stage name + "stage_progress": float, # Always 0.0 + "stage_current": int, # Current count (0) + "stage_total": int # Total items in stage + } + - progress_update: { + "type": "progress_update", + "stage": str, # Stage name + "stage_progress": float, # Stage progress (0-100) + "stage_current": int, # Current items processed + "stage_total": int, # Total items in stage + "overall_progress": float # Overall progress (0-100) + } + - progress_end: { + "type": "progress_end", + "stage": str, # Stage name + "stage_progress": float, # Always 100.0 + "stage_current": int, # Equal to stage_total + "stage_total": int, # Total items processed + "overall_progress": float # Overall progress (0-100) + } + - finish: { + "type": "finish", + "translate_result": TranslateResult + } + - error: { + "type": "error", + "error": str + } + + Args: + translation_config: Configuration for the translation process + + Yields: + dict: Progress events during translation + + Raises: + CancelledError: If the translation is cancelled + Exception: Any other errors during translation + """ + loop = asyncio.get_running_loop() + callback = asynchronize.AsyncCallback() + + finish_event = asyncio.Event() + cancel_event = threading.Event() + with ProgressMonitor( + translation_config, + TRANSLATE_STAGES, + progress_change_callback=callback.step_callback, + finish_callback=callback.finished_callback, + finish_event=finish_event, + cancel_event=cancel_event, + loop=loop, + report_interval=translation_config.report_interval, + ) as pm: + future = loop.run_in_executor(None, do_translate, pm, translation_config) + try: + async for event in callback: + event = event.kwargs + yield event + if event["type"] == "error": + break + except CancelledError: + cancel_event.set() + except KeyboardInterrupt: + logger.info("Translation cancelled by user through keyboard interrupt") + cancel_event.set() + if cancel_event.is_set(): + future.cancel() + logger.info("Waiting for translation to finish...") + await finish_event.wait() + + +def do_translate(pm, translation_config): + try: + translation_config.progress_monitor = pm + original_pdf_path = translation_config.input_file + logger.info(f"start to translate: {original_pdf_path}") + start_time = time.time() + doc_input = Document(original_pdf_path) + if translation_config.debug: + logger.debug("debug mode, save decompressed input pdf") + output_path = translation_config.get_working_file_path( + "input.decompressed.pdf" + ) + doc_input.save(output_path, expand=True, pretty=True) + # Continue with original processing + temp_pdf_path = translation_config.get_working_file_path("input.pdf") + doc_pdf2zh = Document(original_pdf_path) + resfont = "china-ss" + for page in doc_pdf2zh: + page.insert_font(resfont, None) + doc_pdf2zh.save(temp_pdf_path) + il_creater = ILCreater(translation_config) + il_creater.mupdf = doc_input + xml_converter = XMLConverter() + logger.debug(f"start parse il from {temp_pdf_path}") + with Path(temp_pdf_path).open("rb") as f: + start_parse_il( + f, + doc_zh=doc_pdf2zh, + resfont=resfont, + il_creater=il_creater, + translation_config=translation_config, + ) + logger.debug(f"finish parse il from {temp_pdf_path}") + docs = il_creater.create_il() + logger.debug(f"finish create il from {temp_pdf_path}") + if translation_config.debug: + xml_converter.write_json( + docs, translation_config.get_working_file_path("create_il.debug.json") + ) + # Generate layouts for all pages + logger.debug("start generating layouts") + docs = LayoutParser(translation_config).process(docs, doc_input) + logger.debug("finish generating layouts") + if translation_config.debug: + xml_converter.write_json( + docs, translation_config.get_working_file_path("layout_generator.json") + ) + ParagraphFinder(translation_config).process(docs) + logger.debug(f"finish paragraph finder from {temp_pdf_path}") + if translation_config.debug: + xml_converter.write_json( + docs, translation_config.get_working_file_path("paragraph_finder.json") + ) + StylesAndFormulas(translation_config).process(docs) + logger.debug(f"finish styles and formulas from {temp_pdf_path}") + if translation_config.debug: + xml_converter.write_json( + docs, + translation_config.get_working_file_path("styles_and_formulas.json"), + ) + RemoveDescent(translation_config).process(docs) + logger.debug(f"finish remove descent from {temp_pdf_path}") + if translation_config.debug: + xml_converter.write_json( + docs, translation_config.get_working_file_path("remove_descent.json") + ) + translate_engine = translation_config.translator + ILTranslator(translate_engine, translation_config).translate(docs) + logger.debug(f"finish ILTranslator from {temp_pdf_path}") + if translation_config.debug: + xml_converter.write_json( + docs, translation_config.get_working_file_path("il_translated.json") + ) + + if translation_config.debug: + AddDebugInformation(translation_config).process(docs) + xml_converter.write_json( + docs, + translation_config.get_working_file_path("add_debug_information.json"), + ) + + Typesetting(translation_config).typsetting_document(docs) + logger.debug(f"finish typsetting from {temp_pdf_path}") + if translation_config.debug: + xml_converter.write_json( + docs, translation_config.get_working_file_path("typsetting.json") + ) + # deepcopy + # docs2 = xml_converter.deepcopy(docs) + pdf_creater = PDFCreater(temp_pdf_path, docs, translation_config) + result = pdf_creater.write(translation_config) + finish_time = time.time() + result.original_pdf_path = original_pdf_path + result.total_seconds = finish_time - start_time + logger.info( + f"finish translate: {original_pdf_path}, cost: {finish_time - start_time} s" + ) + pm.translate_done(result) + return result + except Exception as e: + logger.exception(f"translate error: {e}") + pm.translate_error(e) + raise + finally: + logger.debug("do_translate finally") + pm.on_finish() + + +def download_font_assets() -> None: + """Download and verify font assets.""" + for name, url, expected_hash in FONT_ASSETS: + save_path = get_cache_file_path(name) + + # Check if file exists and has correct hash + if Path(save_path).exists(): + if verify_file_hash(save_path, expected_hash): + continue + else: + logger.warning(f"Hash mismatch for {name}, re-downloading...") + Path(save_path).unlink() + + # Download file + r = httpx.get(url, follow_redirects=True) + if not r.is_success: + logger.critical("cannot download %s font", name, exc_info=True) + exit(1) + + # Save and verify + with Path(save_path).open("wb") as f: + f.write(r.content) + + if not verify_file_hash(save_path, expected_hash): + logger.critical(f"Downloaded file {name} has incorrect hash!") + Path(save_path).unlink() + exit(1) + + logger.info(f"Successfully downloaded and verified {name}") + + +def create_cache_folder() -> None: + try: + logger.debug(f"create cache folder at {CACHE_FOLDER}") + Path(CACHE_FOLDER).mkdir(parents=True, exist_ok=True) + except OSError: + logger.critical( + f"Failed to create cache folder at {CACHE_FOLDER}", exc_info=True + ) + exit(1) + + +def init() -> None: + create_cache_folder() + download_font_assets() diff --git a/src/pdf2u/io.py b/src/pdf2u/io.py new file mode 100644 index 0000000000000000000000000000000000000000..b31cb950572db44c2136db59441b003a161646f4 --- /dev/null +++ b/src/pdf2u/io.py @@ -0,0 +1,8 @@ +import json +from pathlib import Path +from typing import Any + + +def load_json(file_path: Path) -> dict[str, Any]: + with open(file_path) as f: + return json.load(f) diff --git a/src/pdf2u/main.py b/src/pdf2u/main.py new file mode 100644 index 0000000000000000000000000000000000000000..95136def886acfbe5bbbc903cc3d6bf32da1973f --- /dev/null +++ b/src/pdf2u/main.py @@ -0,0 +1,302 @@ +import asyncio +import logging +from pathlib import Path +from typing import Any + +import tqdm # type: ignore +import typer +from rich.logging import RichHandler +from rich.progress import ( + BarColumn, + MofNCompleteColumn, + Progress, + TextColumn, + TimeElapsedColumn, + TimeRemainingColumn, +) + +import pdf2u.high_level +from pdf2u.const import TranslationService, get_cache_file_path +from pdf2u.document_il.translator.translator import ( + BaseTranslator, + BingTranslator, + GoogleTranslator, + OpenAITranslator, + set_translate_rate_limiter, +) +from pdf2u.io import load_json +from pdf2u.translation_config import TranslationConfig + +# Initialize typer app +pdf2u.high_level.init() +app = typer.Typer(help="PDF translation tool") + +# Setup logging +logging.basicConfig(level=logging.INFO, handlers=[RichHandler()]) +logger = logging.getLogger(__name__) + +# Disable noisy loggers +NOISY_LOGGERS = ["httpx", "openai", "httpcore", "http11", "peewee"] + +for logger_name in NOISY_LOGGERS: + log = logging.getLogger(logger_name) + log.setLevel("CRITICAL") + log.propagate = False + + +def create_progress_handler(translation_config: TranslationConfig): # type: ignore + """Create a progress handler function based on the configuration.""" + if translation_config.use_rich_pbar: + progress = Progress( + TextColumn("[progress.description]{task.description}"), + BarColumn(), + MofNCompleteColumn(), + TimeElapsedColumn(), + TimeRemainingColumn(), + ) + translate_task_id = progress.add_task("translate", total=100) + stage_tasks = {} + + def progress_handler(event: dict[str, Any]) -> None: + if event["type"] == "progress_start": + stage_tasks[event["stage"]] = progress.add_task( + f"{event['stage']}", total=event.get("stage_total", 100) + ) + elif event["type"] == "progress_update": + stage = event["stage"] + if stage in stage_tasks: + progress.update( + stage_tasks[stage], + completed=event["stage_current"], + total=event["stage_total"], + description=f"{event['stage']} ({event['stage_current']}/{event['stage_total']})", + refresh=True, + ) + progress.update( + translate_task_id, completed=event["overall_progress"], refresh=True + ) + elif event["type"] == "progress_end": + stage = event["stage"] + if stage in stage_tasks: + progress.update( + stage_tasks[stage], + completed=event["stage_total"], + total=event["stage_total"], + description=f"{event['stage']} (Complete)", + refresh=True, + ) + progress.update( + translate_task_id, + completed=event["overall_progress"], + refresh=True, + ) + progress.refresh() + + return progress, progress_handler + else: + pbar = tqdm.tqdm(total=100, desc="translate") + + def progress_handler(event: dict[str, Any]) -> None: + if event["type"] == "progress_update": + pbar.update(event["overall_progress"] - pbar.n) + pbar.set_description( + f"{event['stage']} ({event['stage_current']}/{event['stage_total']})" + ) + elif event["type"] == "progress_end": + pbar.set_description(f"{event['stage']} (Complete)") + pbar.refresh() + + return pbar, progress_handler + + +async def process_file(config: TranslationConfig, debug: bool) -> None: + """Process a single PDF file.""" + progress_context, progress_handler = create_progress_handler(config) + + with progress_context: + async for event in pdf2u.high_level.async_translate(config): + progress_handler(event) + if debug: + logger.debug(event) + if event["type"] == "finish": + result = event["translate_result"] + logger.info("Translation Result:") + logger.info(f" Original PDF: {result.original_pdf_path}") + logger.info(f" Time Cost: {result.total_seconds:.2f}s") + logger.info(f" Mono PDF: {result.mono_pdf_path or 'None'}") + logger.info(f" Dual PDF: {result.dual_pdf_path or 'None'}") + break + + +@app.command() +def version() -> None: + """Print the version of pdf2u.""" + typer.echo(pdf2u.__version__) + + +@app.command() +def translate( + files: list[Path] = typer.Option( + ..., "--files", "-f", help="PDF files to translate" + ), + config_file: Path | None = typer.Option( + None, "--config", "-c", help="Path to JSON config file" + ), + service: TranslationService = typer.Option( + None, "--service", "-s", help="Translation service to use" + ), + openai_api_key: str | None = typer.Option( + None, "--openai-api-key", "-k", help="OpenAI API key" + ), + openai_model: str = typer.Option( + "gpt-4-mini", "--openai-model", "-m", help="OpenAI model name" + ), + openai_base_url: str | None = typer.Option( + None, "--openai-base-url", "-b", help="OpenAI API base URL" + ), + lang_in: str = typer.Option("en", "--lang-in", "-li", help="Source language"), + lang_out: str = typer.Option("zh", "--lang-out", "-lo", help="Target language"), + output: Path | None = typer.Option(None, "--output", "-o", help="Output directory"), + pages: str | None = typer.Option( + None, "--pages", "-p", help="Pages to translate (e.g. '1,2,1-,-3,3-5')" + ), + qps: int = typer.Option(4, "--qps", "-q", help="QPS limit for translation"), + min_text_length: int = typer.Option( + 5, "--min-text-length", help="Minimum text length to translate" + ), + debug: bool = typer.Option(False, "--debug", "-d", help="Enable debug logging"), + ignore_cache: bool = typer.Option( + False, "--ignore-cache", "-ic", help="Ignore translation cache" + ), + no_dual: bool = typer.Option(False, "--no-dual", help="Don't output bilingual PDF"), + no_mono: bool = typer.Option( + False, "--no-mono", help="Don't output monolingual PDF" + ), + skip_clean: bool = typer.Option(False, "--skip-clean", help="Skip PDF cleaning"), + enhance_compatibility: bool = typer.Option( + False, "--enhance-compatibility", help="Enable all compatibility enhancements" + ), + rpc_doclayout: str | None = typer.Option( + None, "--rpc-doclayout", help="RPC service host for document layout" + ), +) -> None: + """Translate PDF files using various translation services.""" + if isinstance(config_file, Path): + if not config_file.exists(): + typer.echo(f"Error: Config file not found: {config_file}") + raise typer.Exit(1) + conf = load_json(config_file) + service = conf.get("service", service) + openai_api_key = conf.get("openai_api_key", openai_api_key) + openai_model = conf.get("openai_model", openai_model) + openai_base_url = conf.get("openai_base_url", openai_base_url) + lang_in = conf.get("lang_in", lang_in) + lang_out = conf.get("lang_out", lang_out) + output = Path(conf.get("output", output)) if conf.get("output") else output + pages = conf.get("pages", pages) + qps = conf.get("qps", qps) + min_text_length = conf.get("min_text_length", min_text_length) + debug = conf.get("debug", debug) + ignore_cache = conf.get("ignore_cache", ignore_cache) + no_dual = conf.get("no_dual", no_dual) + no_mono = conf.get("no_mono", no_mono) + skip_clean = conf.get("skip_clean", skip_clean) + enhance_compatibility = conf.get("enhance_compatibility", enhance_compatibility) + rpc_doclayout = conf.get("rpc_doclayout", rpc_doclayout) + + if debug: + logging.getLogger().setLevel(logging.DEBUG) + + # Validate translation service + if not service: + typer.echo("Error: Must specify a translation service (--service)") + raise typer.Exit(1) + + # Validate OpenAI settings + if service == TranslationService.OPENAI and not openai_api_key: + typer.echo("Error: OpenAI API key required when using OpenAI service") + raise typer.Exit(1) + + # Initialize translator + translator: BaseTranslator + if service == TranslationService.OPENAI: + translator = OpenAITranslator( + lang_in=lang_in, + lang_out=lang_out, + model=openai_model, + base_url=openai_base_url, + api_key=openai_api_key, + ignore_cache=ignore_cache, + ) + elif service == TranslationService.BING: + translator = BingTranslator( + lang_in=lang_in, lang_out=lang_out, ignore_cache=ignore_cache + ) + else: # Google + translator = GoogleTranslator( + lang_in=lang_in, lang_out=lang_out, ignore_cache=ignore_cache + ) + + # Set translation rate limit + set_translate_rate_limiter(qps) + + # Initialize document layout model + doc_layout_model: DocLayoutModel + if rpc_doclayout: + from pdf2u.docvision.rpc_doclayout import RpcDocLayoutModel + + doc_layout_model = RpcDocLayoutModel(host=rpc_doclayout) + else: + from pdf2u.docvision.doclayout import DocLayoutModel + + doc_layout_model = DocLayoutModel.load_onnx() + + # Validate files + for file in files: + if not file.exists(): + typer.echo(f"Error: File does not exist: {file}") + raise typer.Exit(1) + if not file.suffix == ".pdf": + typer.echo(f"Error: Not a PDF file: {file}") + raise typer.Exit(1) + + # Get and validate font + font_path = get_cache_file_path("source-han-serif-cn.ttf") + if font_path: + if not Path(font_path).exists(): + typer.echo(f"Error: Font file not found: {font_path}") + raise typer.Exit(1) + if not str(font_path).endswith(".ttf"): + typer.echo(f"Error: Not a TTF font file: {font_path}") + raise typer.Exit(1) + + # Create output directory if needed + if output: + output.mkdir(parents=True, exist_ok=True) + + # Process each file + for file in files: + config = TranslationConfig( + input_file=str(file), + font=font_path.as_posix() if font_path else None, + pages=pages, + output_dir=str(output) if output else None, + translator=translator, + debug=debug, + lang_in=lang_in, + lang_out=lang_out, + no_dual=no_dual, + no_mono=no_mono, + qps=qps, + doc_layout_model=doc_layout_model, + skip_clean=skip_clean, + enhance_compatibility=enhance_compatibility, + min_text_length=min_text_length, + ) + + # Run async process_file in event loop + asyncio.run(process_file(config, debug)) + + +if __name__ == "__main__": + app() diff --git a/src/pdf2u/pdfinterp.py b/src/pdf2u/pdfinterp.py new file mode 100644 index 0000000000000000000000000000000000000000..767a262cb390d447a281761502d86909a8ceef76 --- /dev/null +++ b/src/pdf2u/pdfinterp.py @@ -0,0 +1,445 @@ +import logging +from collections.abc import Sequence +from typing import Any, cast + +import numpy as np +from pdfminer import settings +from pdfminer.pdfcolor import PREDEFINED_COLORSPACE, PDFColorSpace +from pdfminer.pdfdevice import PDFDevice, PDFTextSeq +from pdfminer.pdffont import PDFFont +from pdfminer.pdfinterp import ( + LITERAL_FORM, + LITERAL_IMAGE, + Color, + PDFContentParser, + PDFInterpreterError, + PDFPageInterpreter, + PDFResourceManager, + PDFStackT, +) +from pdfminer.pdfpage import PDFPage +from pdfminer.pdftypes import PDFObjRef, dict_value, list_value, resolve1, stream_value +from pdfminer.psexceptions import PSEOF +from pdfminer.psparser import PSKeyword, keyword_name, literal_name +from pdfminer.utils import MATRIX_IDENTITY, Matrix, Rect, apply_matrix_pt, mult_matrix + +from pdf2u.document_il.frontend.il_creater import ILCreater + +log = logging.getLogger(__name__) + + +def safe_float(o: Any) -> float | None: + try: + return float(o) + except (TypeError, ValueError): + return None + + +class PDFPageInterpreterEx(PDFPageInterpreter): + """Processor for the content of a PDF page + + Reference: PDF Reference, Appendix A, Operator Summary + """ + + def __init__( + self, + rsrcmgr: PDFResourceManager, + device: PDFDevice, + obj_patch, + il_creater: ILCreater, + ) -> None: + self.rsrcmgr = rsrcmgr + self.device = device + self.obj_patch = obj_patch + self.il_creater = il_creater + + def dup(self) -> "PDFPageInterpreterEx": + return self.__class__( + self.rsrcmgr, self.device, self.obj_patch, self.il_creater + ) + + def init_resources(self, resources: dict[object, object]) -> None: + # 重载设置 fontid 和 descent + """Prepare the fonts and XObjects listed in the Resource attribute.""" + self.resources = resources + self.fontmap: dict[object, PDFFont] = {} + self.fontid: dict[PDFFont, object] = {} + self.xobjmap = {} + self.csmap: dict[str, PDFColorSpace] = PREDEFINED_COLORSPACE.copy() + if not resources: + return + + def get_colorspace(spec: object) -> PDFColorSpace | None: + if isinstance(spec, list): + name = literal_name(spec[0]) + else: + name = literal_name(spec) + if name == "ICCBased" and isinstance(spec, list) and len(spec) >= 2: + return PDFColorSpace(name, stream_value(spec[1])["N"]) + elif name == "DeviceN" and isinstance(spec, list) and len(spec) >= 2: + return PDFColorSpace(name, len(list_value(spec[1]))) + else: + return PREDEFINED_COLORSPACE.get(name) + + for k, v in dict_value(resources).items(): + # log.debug("Resource: %r: %r", k, v) + if k == "Font": + for fontid, spec in dict_value(v).items(): + objid = None + if isinstance(spec, PDFObjRef): + objid = spec.objid + spec = dict_value(spec) + font = self.rsrcmgr.get_font(objid, spec) + self.il_creater.on_page_resource_font(font, objid, fontid) + self.fontmap[fontid] = font + self.fontmap[fontid].descent = 0 # hack fix descent + self.fontid[self.fontmap[fontid]] = fontid + elif k == "ColorSpace": + for csid, spec in dict_value(v).items(): + colorspace = get_colorspace(resolve1(spec)) + if colorspace is not None: + self.csmap[csid] = colorspace + elif k == "ProcSet": + self.rsrcmgr.get_procset(list_value(v)) + elif k == "XObject": + for xobjid, xobjstrm in dict_value(v).items(): + self.xobjmap[xobjid] = xobjstrm + pass + + def do_S(self) -> None: + # 重载过滤非公式线条 + """Stroke path""" + + def is_black(color: Color) -> bool: + if isinstance(color, tuple): + return sum(color) == 0 + else: + return color == 0 + + if ( + len(self.curpath) == 2 + and self.curpath[0][0] == "m" + and self.curpath[1][0] == "l" + and apply_matrix_pt(self.ctm, self.curpath[0][-2:])[1] + == apply_matrix_pt(self.ctm, self.curpath[1][-2:])[1] + and is_black(self.graphicstate.scolor) + ): # 独立直线,水平,黑色 + # print(apply_matrix_pt(self.ctm,self.curpath[0][-2:]),apply_matrix_pt(self.ctm,self.curpath[1][-2:]),self.graphicstate.scolor) + self.device.paint_path(self.graphicstate, True, False, False, self.curpath) + self.curpath = [] + return "n" + else: + self.curpath = [] + + def do_CS(self, name: PDFStackT) -> None: + """Set color space for stroking operations + + Introduced in PDF 1.1 + """ + try: + self.il_creater.on_stroking_color_space(literal_name(name)) + self.scs = self.csmap[literal_name(name)] + except KeyError: + if settings.STRICT: + raise PDFInterpreterError(f"Undefined ColorSpace: {name!r}") from None + return + + def do_cs(self, name: PDFStackT) -> None: + """Set color space for nonstroking operations""" + try: + self.il_creater.on_non_stroking_color_space(literal_name(name)) + self.ncs = self.csmap[literal_name(name)] + except KeyError: + if settings.STRICT: + raise PDFInterpreterError(f"Undefined ColorSpace: {name!r}") from None + return + + ############################################################ + # 重载过滤非公式线条(F/B) + def do_f(self) -> None: + """Fill path using nonzero winding number rule""" + # self.device.paint_path(self.graphicstate, False, True, False, self.curpath) + self.curpath = [] + + def do_F(self) -> None: + """Fill path using nonzero winding number rule (obsolete)""" + + def do_f_a(self) -> None: + """Fill path using even-odd rule""" + # self.device.paint_path(self.graphicstate, False, True, True, self.curpath) + self.curpath = [] + + def do_B(self) -> None: + """Fill and stroke path using nonzero winding number rule""" + # self.device.paint_path(self.graphicstate, True, True, False, self.curpath) + self.curpath = [] + + def do_B_a(self) -> None: + """Fill and stroke path using even-odd rule""" + # self.device.paint_path(self.graphicstate, True, True, True, self.curpath) + self.curpath = [] + + ############################################################ + # 重载返回调用参数(SCN) + def do_SCN(self) -> None: + """Set color for stroking operations.""" + if self.scs: + n = self.scs.ncomponents + else: + if settings.STRICT: + raise PDFInterpreterError("No colorspace specified!") + n = 1 + args = self.pop(n) + self.il_creater.on_passthrough_per_char("SCN", args) + self.graphicstate.scolor = cast(Color, args) + return args + + def do_scn(self) -> None: + """Set color for nonstroking operations""" + if self.ncs: + n = self.ncs.ncomponents + else: + if settings.STRICT: + raise PDFInterpreterError("No colorspace specified!") + n = 1 + args = self.pop(n) + self.il_creater.on_passthrough_per_char("scn", args) + self.graphicstate.ncolor = cast(Color, args) + return args + + def do_SC(self) -> None: + """Set color for stroking operations""" + args = self.do_SCN() + self.il_creater.remove_latest_passthrough_per_char_instruction() + self.il_creater.on_passthrough_per_char("SC", args) + return args + + def do_sc(self) -> None: + """Set color for nonstroking operations""" + args = self.do_scn() + self.il_creater.remove_latest_passthrough_per_char_instruction() + self.il_creater.on_passthrough_per_char("sc", args) + return args + + def do_Do(self, xobjid_arg: PDFStackT) -> None: + # 重载设置 xobj 的 obj_patch + """Invoke named XObject""" + xobjid = literal_name(xobjid_arg) + try: + xobj = stream_value(self.xobjmap[xobjid]) + except KeyError: + if settings.STRICT: + raise PDFInterpreterError(f"Undefined xobject id: {xobjid!r}") from None + return + # log.debug("Processing xobj: %r", xobj) + subtype = xobj.get("Subtype") + if subtype is LITERAL_FORM and "BBox" in xobj: + interpreter = self.dup() + bbox = cast(Rect, list_value(xobj["BBox"])) + matrix = cast(Matrix, list_value(xobj.get("Matrix", MATRIX_IDENTITY))) + # According to PDF reference 1.7 section 4.9.1, XObjects in + # earlier PDFs (prior to v1.2) use the page's Resources entry + # instead of having their own Resources entry. + xobjres = xobj.get("Resources") + if xobjres: + resources = dict_value(xobjres) + else: + resources = self.resources.copy() + self.device.begin_figure(xobjid, bbox, matrix) + ctm = mult_matrix(matrix, self.ctm) + (x, y, x2, y2) = bbox + (x, y) = apply_matrix_pt(ctm, (x, y)) + (x2, y2) = apply_matrix_pt(ctm, (x2, y2)) + x_id = self.il_creater.on_xobj_begin((x, y, x2, y2), xobj.objid) + ctm_inv = np.linalg.inv(np.array(ctm[:4]).reshape(2, 2)) + np_version = np.__version__ + if np_version.split(".")[0] >= "2": + pos_inv = -np.asmatrix(ctm[4:]) * ctm_inv + else: + pos_inv = -np.mat(ctm[4:]) * ctm_inv + a, b, c, d = ctm_inv.reshape(4).tolist() + e, f = pos_inv.tolist()[0] + ops_base = interpreter.render_contents(resources, [xobj], ctm=ctm) + self.ncs = interpreter.ncs + self.scs = interpreter.scs + self.il_creater.on_xobj_end( + x_id, f"q {ops_base}Q {a} {b} {c} {d} {e} {f} cm " + ) + try: # 有的时候 form 字体加不上这里会烂掉 + self.device.fontid = interpreter.fontid + self.device.fontmap = interpreter.fontmap + ops_new = self.device.end_figure(xobjid) + ctm_inv = np.linalg.inv(np.array(ctm[:4]).reshape(2, 2)) + np_version = np.__version__ + if np_version.split(".")[0] >= "2": + pos_inv = -np.asmatrix(ctm[4:]) * ctm_inv + else: + pos_inv = -np.mat(ctm[4:]) * ctm_inv + a, b, c, d = ctm_inv.reshape(4).tolist() + e, f = pos_inv.tolist()[0] + self.obj_patch[self.xobjmap[xobjid].objid] = ( + f"q {ops_base}Q {a} {b} {c} {d} {e} {f} cm {ops_new}" + ) + except Exception: + pass + elif subtype is LITERAL_IMAGE and "Width" in xobj and "Height" in xobj: + self.device.begin_figure(xobjid, (0, 0, 1, 1), MATRIX_IDENTITY) + self.device.render_image(xobjid, xobj) + self.device.end_figure(xobjid) + else: + # unsupported xobject type. + pass + + def process_page(self, page: PDFPage) -> None: + # 重载设置 page 的 obj_patch + # log.debug("Processing page: %r", page) + # print(page.mediabox,page.cropbox) + # (x0, y0, x1, y1) = page.mediabox + (x0, y0, x1, y1) = page.cropbox + if page.rotate == 90: + ctm = (0, -1, 1, 0, -y0, x1) + elif page.rotate == 180: + ctm = (-1, 0, 0, -1, x1, y1) + elif page.rotate == 270: + ctm = (0, 1, -1, 0, y1, -x0) + else: + ctm = (1, 0, 0, 1, -x0, -y0) + self.il_creater.on_page_start() + self.il_creater.on_page_crop_box(x0, y0, x1, y1) + self.device.begin_page(page, ctm) + ops_base = self.render_contents(page.resources, page.contents, ctm=ctm) + self.device.fontid = self.fontid + self.device.fontmap = self.fontmap + ops_new = self.device.end_page(page) + # 上面渲染的时候会根据 cropbox 减掉页面偏移得到真实坐标,这里输出的时候需要用 cm 把页面偏移加回来 + self.obj_patch[page.page_xref] = ( + # f"q {ops_base}Q 1 0 0 1 {x0} {y0} cm {ops_new}" # ops_base 里可能有图,需要让 ops_new 里的文字覆盖在上面,使用 q/Q 重置位置矩阵 + "" + ) + for obj in page.contents: + self.obj_patch[obj.objid] = "" + return ops_base + + def render_contents( + self, + resources: dict[object, object], + streams: Sequence[object], + ctm: Matrix = MATRIX_IDENTITY, + ) -> None: + # 重载返回指令流 + """Render the content streams. + + This method may be called recursively. + """ + # log.debug( + # "render_contents: resources=%r, streams=%r, ctm=%r", + # resources, + # streams, + # ctm, + # ) + self.init_resources(resources) + self.init_state(ctm) + return self.execute(list_value(streams)) + + def do_q(self) -> None: + """Save graphics state""" + self.gstack.append(self.get_current_state()) + self.il_creater.push_passthrough_per_char_instruction() + return + + def do_Q(self) -> None: + """Restore graphics state""" + if self.gstack: + self.set_current_state(self.gstack.pop()) + self.il_creater.pop_passthrough_per_char_instruction() + return + + def do_TJ(self, seq: PDFStackT) -> None: + """Show text, allowing individual glyph positioning""" + if self.textstate.font is None: + if settings.STRICT: + raise PDFInterpreterError("No font specified!") + return + assert self.ncs is not None + gs = self.graphicstate.copy() + gs.passthrough_instruction = ( + self.il_creater.passthrough_per_char_instruction.copy() + ) + self.device.render_string(self.textstate, cast(PDFTextSeq, seq), self.ncs, gs) + return + + # Run PostScript commands + # The Do_xxx method is the method for executing corresponding postscript instructions + def execute(self, streams: Sequence[object]) -> None: + ops = "" + for stream in streams: + self.il_creater.on_new_stream() + # 重载返回指令流 + try: + parser = PDFContentParser([stream]) + except PSEOF: + # empty page + return + while True: + try: + (_, obj) = parser.nextobject() + except PSEOF: + break + if isinstance(obj, PSKeyword): + name = keyword_name(obj) + act_name = ( + name.replace("*", "_a").replace('"', "_w").replace("'", "_q") + ) + method = f"do_{act_name}" + if hasattr(self, method): + func = getattr(self, method) + nargs = func.__code__.co_argcount - 1 + if nargs: + args = self.pop(nargs) + # log.debug("exec: %s %r", name, args) + if len(args) == nargs: + func(*args) + if self.il_creater.is_passthrough_per_char_operation( + name + ): + self.il_creater.on_passthrough_per_char(name, args) + if not ( + name[0] == "T" + or name + in ['"', "'", "EI", "MP", "DP", "BMC", "BDC"] + ): # 过滤 T 系列文字指令,因为 EI 的参数是 obj 所以也需要过滤(只在少数文档中画横线时使用),过滤 marked 系列指令 + p = " ".join( + [ + ( + f"{x:f}" + if isinstance(x, float) + else str(x).replace("'", "") + ) + for x in args + ] + ) + ops += f"{p} {name} " + else: + # log.debug("exec: %s", name) + targs = func() + if targs is None: + targs = [] + if not (name[0] == "T" or name in ["BI", "ID", "EMC"]): + p = " ".join( + [ + ( + f"{x:f}" + if isinstance(x, float) + else str(x).replace("'", "") + ) + for x in targs + ] + ) + ops += f"{p} {name} " + elif settings.STRICT: + error_msg = f"Unknown operator: {name!r}" + raise PDFInterpreterError(error_msg) + else: + self.push(obj) + # print('REV DATA',ops) + return ops diff --git a/src/pdf2u/progress_monitor.py b/src/pdf2u/progress_monitor.py new file mode 100644 index 0000000000000000000000000000000000000000..da1d07c54d8d4dba9265e46e3c8dfa69e5e22732 --- /dev/null +++ b/src/pdf2u/progress_monitor.py @@ -0,0 +1,158 @@ +import asyncio +import logging +import threading +import time +from asyncio import CancelledError + +logger = logging.getLogger(__name__) + + +class ProgressMonitor: + def __init__( + self, + translation_config, + stages: list[str], + progress_change_callback: callable = None, + finish_callback: callable = None, + report_interval: float = 0.1, + finish_event: asyncio.Event = None, + cancel_event: threading.Event = None, + loop: asyncio.AbstractEventLoop | None = None, + ): + self.stage = {k: TranslationStage(k, 0, self) for k in stages} + self.translation_config = translation_config + self.progress_change_callback = progress_change_callback + self.finish_callback = finish_callback + self.report_interval = report_interval + logger.debug(f"report_interval: {self.report_interval}") + self.last_report_time = 0 + self.finish_stage_count = 0 + self.finish_event = finish_event + self.cancel_event = cancel_event + self.loop = loop + if finish_event and not loop: + raise ValueError("finish_event requires a loop") + if self.progress_change_callback: + self.progress_change_callback( + type="stage_summary", + stages=[ + {"name": name, "percent": 1.0 / len(stages)} for name in stages + ], + ) + self.lock = threading.Lock() + + def stage_start(self, stage_name: str, total: int): + stage = self.stage[stage_name] + stage.run_time += 1 + stage.name = stage_name + stage.display_name = ( + f"{stage_name} ({stage.run_time})" if stage.run_time > 1 else stage_name + ) + stage.current = 0 + stage.total = total + if self.progress_change_callback: + self.progress_change_callback( + type="progress_start", + stage=stage.display_name, + stage_progress=0.0, + stage_current=0, + stage_total=total, + overall_progress=self.calculate_current_progress(), + ) + self.last_report_time = 0.0 + return stage + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + logger.debug("ProgressMonitor __exit__") + + def on_finish(self): + if self.cancel_event: + self.cancel_event.set() + if self.finish_event and self.loop: + self.loop.call_soon_threadsafe(self.finish_event.set) + if self.cancel_event and self.cancel_event.is_set(): + self.finish_callback(type="error", error=CancelledError) + + def stage_done(self, stage): + self.last_report_time = 0.0 + self.finish_stage_count += 1 + if ( + stage.current != stage.total + and self.cancel_event is not None + and not self.cancel_event.is_set() + ): + logger.warning( + f"Stage {stage.name} completed with {stage.current}/{stage.total} items" + ) + return + if self.progress_change_callback: + self.progress_change_callback( + type="progress_end", + stage=stage.display_name, + stage_progress=100.0, + stage_current=stage.total, + stage_total=stage.total, + overall_progress=self.calculate_current_progress(), + ) + + def calculate_current_progress(self, stage=None): + progress = self.finish_stage_count * 100 / len(self.stage) + if stage is not None: + progress += stage.current * 100 / stage.total / len(self.stage) + return progress + + def stage_update(self, stage, n: int): + with self.lock: + report_time_delta = time.time() - self.last_report_time + if report_time_delta < self.report_interval and stage.total > 3: + return + if self.progress_change_callback: + self.progress_change_callback( + type="progress_update", + stage=stage.display_name, + stage_progress=stage.current * 100 / stage.total, + stage_current=stage.current, + stage_total=stage.total, + overall_progress=self.calculate_current_progress(stage), + ) + self.last_report_time = time.time() + + def translate_done(self, translate_result): + if self.finish_callback: + self.finish_callback(type="finish", translate_result=translate_result) + + def translate_error(self, error): + if self.finish_callback: + self.finish_callback(type="error", error=error) + + def raise_if_cancelled(self): + if self.cancel_event and self.cancel_event.is_set(): + logger.info("Translation canceled") + raise asyncio.CancelledError + + def cancel(self): + if self.cancel_event: + self.cancel_event.set() + + +class TranslationStage: + def __init__(self, name: str, total: int, pm: ProgressMonitor): + self.name = name + self.display_name = name + self.current = 0 + self.total = total + self.pm = pm + self.run_time = 0 + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.pm.stage_done(self) + + def advance(self, n: int = 1): + self.current += n + self.pm.stage_update(self, n) diff --git a/src/pdf2u/py.typed b/src/pdf2u/py.typed new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/pdf2u/translation_config.py b/src/pdf2u/translation_config.py new file mode 100644 index 0000000000000000000000000000000000000000..837c9c659d0681d91f79a0bc9044a1b45d0efc8f --- /dev/null +++ b/src/pdf2u/translation_config.py @@ -0,0 +1,166 @@ +import contextlib +import shutil +import tempfile +import threading +from pathlib import Path + +from pdf2u.const import CACHE_FOLDER +from pdf2u.document_il.translator.translator import BaseTranslator +from pdf2u.docvision.doclayout import DocLayoutModel +from pdf2u.progress_monitor import ProgressMonitor + + +class TranslationConfig: + def __init__( + self, + input_file: str, + translator: BaseTranslator, + lang_in: str, + lang_out: str, + font: str | None = None, + pages: str | None = None, + output_dir: str | None = None, + debug: bool = False, + working_dir: str | None = None, + no_dual: bool = False, + no_mono: bool = False, + formular_font_pattern: str | None = None, + formular_char_pattern: str | None = None, + qps: int = 1, + split_short_lines: bool = False, # 是否将比较短的行强制切分成不同段落,此功能可能会导致糟糕的排版&bug + short_line_split_factor: float = 0.8, # 切分阈值系数。实际阈值为当前页所有行长度中位数*此系数 + use_rich_pbar: bool = True, # 是否使用 rich 进度条 + progress_monitor: ProgressMonitor | None = None, # progress_monitor + doc_layout_model=None, + skip_clean: bool = False, + dual_translate_first: bool = False, + disable_rich_text_translate: bool = False, # 是否禁用富文本翻译 + enhance_compatibility: bool = False, # 增强兼容性模式 + report_interval: float = 0.1, # Progress report interval in seconds + min_text_length: int = 5, # Minimum text length to translate + ): + self.input_file = input_file + self.translator = translator + self.font = font + self.pages = pages + self.page_ranges = self._parse_pages(pages) if pages else None + self.debug = debug + self.lang_in = lang_in + self.lang_out = lang_out + self.no_dual = no_dual + self.no_mono = no_mono + self.formular_font_pattern = formular_font_pattern + self.formular_char_pattern = formular_char_pattern + self.qps = qps + self.split_short_lines = split_short_lines + self.short_line_split_factor = short_line_split_factor + self.use_rich_pbar = use_rich_pbar + self.progress_monitor = progress_monitor + self.skip_clean = skip_clean or enhance_compatibility + self.dual_translate_first = dual_translate_first or enhance_compatibility + self.disable_rich_text_translate = ( + disable_rich_text_translate or enhance_compatibility + ) + self.report_interval = report_interval + self.min_text_length = min_text_length + if progress_monitor: + if progress_monitor.cancel_event is None: + progress_monitor.cancel_event = threading.Event() + if progress_monitor.finish_event is None: + progress_monitor.finish_event = threading.Event() + + if working_dir is None: + if debug: + working_dir = ( + Path(CACHE_FOLDER) / "working" / Path(input_file).name.split(".")[0] + ) + else: + working_dir = tempfile.mkdtemp() + self.working_dir = working_dir + self._is_temp_dir = not debug and working_dir.startswith(tempfile.gettempdir()) + + Path(working_dir).mkdir(parents=True, exist_ok=True) + + if output_dir is None: + # output_dir = os.path.dirname(input_file) + output_dir = Path.cwd() + self.output_dir = output_dir + + Path(output_dir).mkdir(parents=True, exist_ok=True) + + if doc_layout_model is None: + doc_layout_model = DocLayoutModel.load_available() + self.doc_layout_model = doc_layout_model + + def get_output_file_path(self, filename): + return Path(self.output_dir) / filename + + def get_working_file_path(self, filename): + return Path(self.working_dir) / filename + + def _parse_pages(self, pages_str: str | None) -> list[tuple[int, int]] | None: + """解析页码字符串,返回页码范围列表 + + Args: + pages_str: 形如 "1-,2,-3,4" 的页码字符串 + + Returns: + 包含 (start, end) 元组的列表,其中 -1 表示无限制 + """ + if not pages_str: + return None + + ranges = [] + for part in pages_str.split(","): + part = part.strip() + if "-" in part: + start, end = part.split("-") + start = int(start) if start else 1 + end = int(end) if end else -1 + ranges.append((start, end)) + else: + page = int(part) + ranges.append((page, page)) + return ranges + + def should_translate_page(self, page_number: int) -> bool: + """判断指定页码是否需要翻译 + + Args: + page_number: 页码 + + Returns: + 是否需要翻译该页 + """ + if not self.page_ranges: + return True + + for start, end in self.page_ranges: + if start <= page_number and (end == -1 or page_number <= end): + return True + return False + + def raise_if_cancelled(self): + if self.progress_monitor: + self.progress_monitor.raise_if_cancelled() + + def cancel_translation(self): + if self.progress_monitor: + self.progress_monitor.cancel() + + def __del__(self): + """Clean up temporary directory if it was created.""" + if hasattr(self, "_is_temp_dir") and self._is_temp_dir: + with contextlib.suppress(Exception): + shutil.rmtree(self.working_dir, ignore_errors=True) + + +class TranslateResult: + original_pdf_path: str + total_seconds: float + mono_pdf_path: str | None + dual_pdf_path: str | None + + def __init__(self, mono_pdf_path: str | None, dual_pdf_path: str | None): + self.mono_pdf_path = mono_pdf_path + self.dual_pdf_path = dual_pdf_path diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000000000000000000000000000000000000..f766db630876d6136906aef5649df81c62995ae4 --- /dev/null +++ b/uv.lock @@ -0,0 +1,2229 @@ +version = 1 +requires-python = ">=3.10, <3.13" +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] + +[[package]] +name = "altair" +version = "5.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "jsonschema" }, + { name = "narwhals" }, + { name = "packaging" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/b1/f2969c7bdb8ad8bbdda031687defdce2c19afba2aa2c8e1d2a17f78376d8/altair-5.5.0.tar.gz", hash = "sha256:d960ebe6178c56de3855a68c47b516be38640b73fb3b5111c2a9ca90546dd73d", size = 705305 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/f3/0b6ced594e51cc95d8c1fc1640d3623770d01e4969d29c0bd09945fafefa/altair-5.5.0-py3-none-any.whl", hash = "sha256:91a310b926508d560fe0148d02a194f38b824122641ef528113d029fcd129f8c", size = 731200 }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "anyio" +version = "4.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 }, +] + +[[package]] +name = "attrs" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/7c/fdf464bcc51d23881d110abd74b512a42b3d5d376a55a831b44c603ae17f/attrs-25.1.0.tar.gz", hash = "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e", size = 810562 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/30/d4986a882011f9df997a55e6becd864812ccfcd821d64aac8570ee39f719/attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a", size = 63152 }, +] + +[[package]] +name = "bitarray" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/62/dcfac53d22ef7e904ed10a8e710a36391d2d6753c34c869b51bfc5e4ad54/bitarray-3.0.0.tar.gz", hash = "sha256:a2083dc20f0d828a7cdf7a16b20dae56aab0f43dc4f347a3b3039f6577992b03", size = 126627 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/f7/2cd02557fa9f177d54b51e6d668696266bdc1af978b5c27179449cbf5870/bitarray-3.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5ddbf71a97ad1d6252e6e93d2d703b624d0a5b77c153b12f9ea87d83e1250e0c", size = 172224 }, + { url = "https://files.pythonhosted.org/packages/49/0a/0362089c127f2639041171803f6bf193a9e1deba72df637ebd36cb510f46/bitarray-3.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0e7f24a0b01e6e6a0191c50b06ca8edfdec1988d9d2b264d669d2487f4f4680", size = 123359 }, + { url = "https://files.pythonhosted.org/packages/c7/ab/a0982708b5ad92d6ec40833846ac954b57b16d9f90551a9da59e4bce79e1/bitarray-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:150b7b29c36d9f1a24779aea723fdfc73d1c1c161dc0ea14990da27d4e947092", size = 121267 }, + { url = "https://files.pythonhosted.org/packages/10/23/134ad08b9e7be3b80575fd4a50c33c79b3b360794dfc2716b6a18bf4dd60/bitarray-3.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8330912be6cb8e2fbfe8eb69f82dee139d605730cadf8d50882103af9ac83bb4", size = 278114 }, + { url = "https://files.pythonhosted.org/packages/c1/a1/df7d0b415207de7c6210403865a5d8a9c920209d542f552a09a02749038a/bitarray-3.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e56ba8be5f17dee0ffa6d6ce85251e062ded2faa3cbd2558659c671e6c3bf96d", size = 292725 }, + { url = "https://files.pythonhosted.org/packages/ec/06/03a636ac237c1860e63f037ccff35f0fec45188c94e55d9df526ee80adc3/bitarray-3.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ffd94b4803811c738e504a4b499fb2f848b2f7412d71e6b517508217c1d7929d", size = 294722 }, + { url = "https://files.pythonhosted.org/packages/17/33/c2a7cb6f0030ea94408c84c4f80f4065b54b2bf1d4080e36fcd0b4c587a2/bitarray-3.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0255bd05ec7165e512c115423a5255a3f301417973d20a80fc5bfc3f3640bcb", size = 278304 }, + { url = "https://files.pythonhosted.org/packages/2c/a3/a06f76dd55d5337ff55585059058761c148da6d1e9e2bc0469d881ba5eb8/bitarray-3.0.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe606e728842389943a939258809dc5db2de831b1d2e0118515059e87f7bbc1a", size = 268807 }, + { url = "https://files.pythonhosted.org/packages/29/48/e8157c422542c308d6a0f5b213b21fef5779c80c00e673e2e4d7b3854d60/bitarray-3.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e89ea59a3ed86a6eb150d016ed28b1bedf892802d0ed32b5659d3199440f3ced", size = 272791 }, + { url = "https://files.pythonhosted.org/packages/ad/53/219d82592b150b580fbc479a718a7c31086627ed4599c9930408f43b6de4/bitarray-3.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cf0cc2e91dd38122dec2e6541efa99aafb0a62e118179218181eff720b4b8153", size = 264821 }, + { url = "https://files.pythonhosted.org/packages/95/08/c47b24fbb34a305531d8d0d7c15f5ab9788478384583a2614b07c2449cf8/bitarray-3.0.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:2d9fe3ee51afeb909b68f97e14c6539ace3f4faa99b21012e610bbe7315c388d", size = 287871 }, + { url = "https://files.pythonhosted.org/packages/77/31/5cdf3dcf407e8fcc5ca07a06f45aaf6b0adb15f38fe6fddd03d5d1fbaf9f/bitarray-3.0.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:37be5482b9df3105bad00fdf7dc65244e449b130867c3879c9db1db7d72e508b", size = 299459 }, + { url = "https://files.pythonhosted.org/packages/b5/26/15b3630dc9bed79fc0e4a5dc92f4b1d30a872ff92f20a8b7acbb7a484bfd/bitarray-3.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0027b8f3bb2bba914c79115e96a59b9924aafa1a578223a7c4f0a7242d349842", size = 271131 }, + { url = "https://files.pythonhosted.org/packages/45/4b/1c8ba97a015d9cf44b54d4488a0005c2e9fb33ff1df38fdcf68d1cb76785/bitarray-3.0.0-cp310-cp310-win32.whl", hash = "sha256:628f93e9c2c23930bd1cfe21c634d6c84ec30f45f23e69aefe1fcd262186d7bb", size = 114230 }, + { url = "https://files.pythonhosted.org/packages/84/54/d883073137d5c245555f66c48f9518c855704b4c619aa92ddd74d6eb2c98/bitarray-3.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:0b655c3110e315219e266b2732609fddb0857bc69593de29f3c2ba74b7d3f51a", size = 121439 }, + { url = "https://files.pythonhosted.org/packages/61/41/321edc0fbf7e8c88552d5ff9ee07777d58e2078f2706c6478bc6651b1945/bitarray-3.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:44c3e78b60070389b824d5a654afa1c893df723153c81904088d4922c3cfb6ac", size = 172452 }, + { url = "https://files.pythonhosted.org/packages/48/92/4c312d6d55ac30dae96749830c9f5007a914efcb591ee0828914078eec9f/bitarray-3.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:545d36332de81e4742a845a80df89530ff193213a50b4cbef937ed5a44c0e5e5", size = 123502 }, + { url = "https://files.pythonhosted.org/packages/75/2c/9f3ed70ffac8e6d2b0880e132d9e5024e4ef9404a24220deca8dbd702f15/bitarray-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8a9eb510cde3fa78c2e302bece510bf5ed494ec40e6b082dec753d6e22d5d1b1", size = 121363 }, + { url = "https://files.pythonhosted.org/packages/48/e0/8ec59416aaa7ca1461a0268c0fe2fbdc8d574ac41e307980f555b773d5f6/bitarray-3.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e3727ab63dfb6bde00b281934e2212bb7529ea3006c0031a556a84d2268bea5", size = 285792 }, + { url = "https://files.pythonhosted.org/packages/b9/8a/fb9d76ecb44a79f02188240278574376e851d0ca81437f433c9e6481d2e5/bitarray-3.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2055206ed653bee0b56628f6a4d248d53e5660228d355bbec0014bdfa27050ae", size = 300848 }, + { url = "https://files.pythonhosted.org/packages/63/c5/067b688553b23e99d61ecf930abf1ad5cb5f80c2ebe6f0e2fe8ecab00b3f/bitarray-3.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:147542299f458bdb177f798726e5f7d39ab8491de4182c3c6d9885ed275a3c2b", size = 303027 }, + { url = "https://files.pythonhosted.org/packages/dc/46/25ebc667907736b2c5c84f4bd8260d9bece8b69719a33db5c3f3dcb281a5/bitarray-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f761184b93092077c7f6b7dad7bd4e671c1620404a76620da7872ceb576a94", size = 286125 }, + { url = "https://files.pythonhosted.org/packages/16/dd/f9a1d84965a992ff42cae5b61536e68fc944f3e31a349b690347d98fc5e0/bitarray-3.0.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e008b7b4ce6c7f7a54b250c45c28d4243cc2a3bbfd5298fa7dac92afda229842", size = 277111 }, + { url = "https://files.pythonhosted.org/packages/16/5b/44f298586a09beb62ec553f9efa06c8a5356d2e230e4080c72cb2800a48f/bitarray-3.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dfea514e665af278b2e1d4deb542de1cd4f77413bee83dd15ae16175976ea8d5", size = 280941 }, + { url = "https://files.pythonhosted.org/packages/28/7c/c6e157332227862727959057ba2987e6710985992b196a81f61995f21e19/bitarray-3.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:66d6134b7bb737b88f1d16478ad0927c571387f6054f4afa5557825a4c1b78e2", size = 272817 }, + { url = "https://files.pythonhosted.org/packages/b7/5d/9f7aaaaf85b5247b4a69b93af60ac7dcfff5545bf544a35517618c4244a0/bitarray-3.0.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3cd565253889940b4ec4768d24f101d9fe111cad4606fdb203ea16f9797cf9ed", size = 295830 }, + { url = "https://files.pythonhosted.org/packages/14/1b/86dd50edd2e0612b092fe4caec3001a24298c9acab5e89a503f002ed3bef/bitarray-3.0.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4800c91a14656789d2e67d9513359e23e8a534c8ee1482bb9b517a4cfc845200", size = 307592 }, + { url = "https://files.pythonhosted.org/packages/d6/8f/45a1f1bcce5fd88d2f0bb2e1ebe8bbb55247edcb8e7a8ef06e4437e2b5e3/bitarray-3.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c2945e0390d1329c585c584c6b6d78be017d9c6a1288f9c92006fe907f69cc28", size = 278971 }, + { url = "https://files.pythonhosted.org/packages/43/2d/948c5718fe901aa58c98cef52b8898a6bea865bea7528cff6c2bc703f9f3/bitarray-3.0.0-cp311-cp311-win32.whl", hash = "sha256:c23286abba0cb509733c6ce8f4013cd951672c332b2e184dbefbd7331cd234c8", size = 114242 }, + { url = "https://files.pythonhosted.org/packages/8c/75/e921ada57bb0bcece5eb515927c031f0bc828f702b8f213639358d9df396/bitarray-3.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:ca79f02a98cbda1472449d440592a2fe2ad96fe55515a0447fa8864a38017cf8", size = 121524 }, + { url = "https://files.pythonhosted.org/packages/4e/2e/2e4beb2b714dc83a9e90ac0e4bacb1a191c71125734f72962ee2a20b9cfb/bitarray-3.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:184972c96e1c7e691be60c3792ca1a51dd22b7f25d96ebea502fe3c9b554f25d", size = 172152 }, + { url = "https://files.pythonhosted.org/packages/e0/1f/9ec96408c060ffc3df5ba64d2b520fd0484cb3393a96691df8f660a43b17/bitarray-3.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:787db8da5e9e29be712f7a6bce153c7bc8697ccc2c38633e347bb9c82475d5c9", size = 123319 }, + { url = "https://files.pythonhosted.org/packages/80/9f/4dd05086308bfcc84ad88c663460a8ad9f5f638f9f96eb5fa08381054db6/bitarray-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2da91ab3633c66999c2a352f0ca9ae064f553e5fc0eca231d28e7e305b83e942", size = 121242 }, + { url = "https://files.pythonhosted.org/packages/55/bb/8865b7380e9d20445bc775079f24f2279a8c0d9ee11d57c49b118d39beaf/bitarray-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7edb83089acbf2c86c8002b96599071931dc4ea5e1513e08306f6f7df879a48b", size = 287463 }, + { url = "https://files.pythonhosted.org/packages/db/8b/779119ee438090a80cbfaa49f96e783651183ab4c25b9760fe360aa7cb31/bitarray-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996d1b83eb904589f40974538223eaed1ab0f62be8a5105c280b9bd849e685c4", size = 301599 }, + { url = "https://files.pythonhosted.org/packages/41/25/78f7ba7fa8ab428767dfb722fc1ea9aac4a9813e348023d8047d8fd32253/bitarray-3.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4817d73d995bd2b977d9cde6050be8d407791cf1f84c8047fa0bea88c1b815bc", size = 304837 }, + { url = "https://files.pythonhosted.org/packages/f7/8d/30a448d3157b4239e635c92fc3b3789a5b87784875ca2776f65bd543d136/bitarray-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d47bc4ff9b0e1624d613563c6fa7b80aebe7863c56c3df5ab238bb7134e8755", size = 288588 }, + { url = "https://files.pythonhosted.org/packages/86/e0/c1f1b595682244f55119d55f280b5a996bcd462b702ec220d976a7566d27/bitarray-3.0.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aca0a9cd376beaccd9f504961de83e776dd209c2de5a4c78dc87a78edf61839b", size = 279002 }, + { url = "https://files.pythonhosted.org/packages/5c/4d/a17626923ad2c9d20ed1625fc5b27a8dfe2d1a3e877083e9422455ec302d/bitarray-3.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:572a61fba7e3a710a8324771322fba8488d134034d349dcd036a7aef74723a80", size = 281898 }, + { url = "https://files.pythonhosted.org/packages/50/d8/5c410580a510e669d9a28bf17675e58843236c55c60fc6dc8f8747808757/bitarray-3.0.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a817ad70c1aff217530576b4f037dd9b539eb2926603354fcac605d824082ad1", size = 274622 }, + { url = "https://files.pythonhosted.org/packages/e7/21/de2e8eda85c5f6a05bda75a00c22c94aee71ef09db0d5cbf22446de74312/bitarray-3.0.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2ac67b658fa5426503e9581a3fb44a26a3b346c1abd17105735f07db572195b3", size = 296930 }, + { url = "https://files.pythonhosted.org/packages/13/7b/7cfad12d77db2932fb745fa281693b0031c3dfd7f2ecf5803be688cc3798/bitarray-3.0.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:12f19ede03e685c5c588ab5ed63167999295ffab5e1126c5fe97d12c0718c18f", size = 309836 }, + { url = "https://files.pythonhosted.org/packages/53/e1/5120fbb8438a0d718e063f70168a2975e03f00ce6b86e74b8eec079cb492/bitarray-3.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcef31b062f756ba7eebcd7890c5d5de84b9d64ee877325257bcc9782288564a", size = 281535 }, + { url = "https://files.pythonhosted.org/packages/73/75/8acebbbb4f85dcca73b8e91dde5d3e1e3e2317b36fae4f5b133c60720834/bitarray-3.0.0-cp312-cp312-win32.whl", hash = "sha256:656db7bdf1d81ec3b57b3cad7ec7276765964bcfd0eb81c5d1331f385298169c", size = 114423 }, + { url = "https://files.pythonhosted.org/packages/ca/56/dadae4d4351b337de6e0269001fb40f3ebe9f72222190456713d2c1be53d/bitarray-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:f785af6b7cb07a9b1e5db0dea9ef9e3e8bb3d74874a0a61303eab9c16acc1999", size = 121680 }, + { url = "https://files.pythonhosted.org/packages/01/6b/405d04ed3d0e46dcc52b9f9ca98b342de5930ed87adcacb86afc830e188b/bitarray-3.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fef4e3b3f2084b4dae3e5316b44cda72587dcc81f68b4eb2dbda1b8d15261b61", size = 119755 }, + { url = "https://files.pythonhosted.org/packages/90/d8/cdfd2d41a836479db66c1d33f2615c37529458427586c8d585fec4c39c5c/bitarray-3.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7e9eee03f187cef1e54a4545124109ee0afc84398628b4b32ebb4852b4a66393", size = 124105 }, + { url = "https://files.pythonhosted.org/packages/12/5d/4214bb7103fa9601332b49fc2fcef73005750581aabe7e13163ad66013cc/bitarray-3.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cb5702dd667f4bb10fed056ffdc4ddaae8193a52cd74cb2cdb54e71f4ef2dd1", size = 124669 }, + { url = "https://files.pythonhosted.org/packages/fc/9b/ecfe49cf03047c8415d71ee931352b11b747525cbff9bc5db9c3592d21da/bitarray-3.0.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:666e44b0458bb2894b64264a29f2cc7b5b2cbcc4c5e9cedfe1fdbde37a8e329a", size = 126520 }, + { url = "https://files.pythonhosted.org/packages/e7/79/190bcac2a23fb5f726d0305b372f73e0bf496a43da0ace4e285e9927fcdb/bitarray-3.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c756a92cf1c1abf01e56a4cc40cb89f0ff9147f2a0be5b557ec436a23ff464d8", size = 122035 }, +] + +[[package]] +name = "bitstring" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bitarray" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/43/52859178f337661e2d496262f9061799d99644012b7b02ab5d7c491c21fd/bitstring-4.3.0.tar.gz", hash = "sha256:81800bc4e00b6508716adbae648e741256355c8dfd19541f76482fb89bee0313", size = 251408 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/e2/ffb466d2325eba94fcae18df609605b99d454b3855491d7bf1023c473911/bitstring-4.3.0-py3-none-any.whl", hash = "sha256:3282a896814813f8fe5fa09dbafac842c57aace1d3bfd94546c6f1ed9aafcbe1", size = 71889 }, +] + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458 }, +] + +[[package]] +name = "bracex" +version = "2.5.post1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/6c/57418c4404cd22fe6275b8301ca2b46a8cdaa8157938017a9ae0b3edf363/bracex-2.5.post1.tar.gz", hash = "sha256:12c50952415bfa773d2d9ccb8e79651b8cdb1f31a42f6091b804f6ba2b4a66b6", size = 26641 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/02/8db98cdc1a58e0abd6716d5e63244658e6e63513c65f469f34b6f1053fd0/bracex-2.5.post1-py3-none-any.whl", hash = "sha256:13e5732fec27828d6af308628285ad358047cec36801598368cb28bc631dbaf6", size = 11558 }, +] + +[[package]] +name = "bump-my-version" +version = "0.32.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "questionary" }, + { name = "rich" }, + { name = "rich-click" }, + { name = "tomlkit" }, + { name = "wcmatch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/5c/ed627df6d8220193392a149973db1e97e7e57a1319534a651b06319b5e29/bump_my_version-0.32.1.tar.gz", hash = "sha256:cb537096cba01a2832972902bfff9e0e0d6e8f8dc9fe31c742096a331262e2aa", size = 1066239 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/78/9e6c5211e5efe4fe03316705e433b2071b489b9f97d37be0d7e25f545b87/bump_my_version-0.32.1-py3-none-any.whl", hash = "sha256:76605d0f98d0627b4cff972b4fcd56ff5423357047d954421fe6b721304e5ceb", size = 57574 }, +] + +[[package]] +name = "cachetools" +version = "5.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080 }, +] + +[[package]] +name = "certifi" +version = "2025.1.31" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191 }, + { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592 }, + { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024 }, + { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188 }, + { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571 }, + { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687 }, + { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211 }, + { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325 }, + { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784 }, + { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564 }, + { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804 }, + { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299 }, + { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 }, + { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, + { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 }, + { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 }, + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 }, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/58/5580c1716040bc89206c77d8f74418caf82ce519aae06450393ca73475d1/charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", size = 198013 }, + { url = "https://files.pythonhosted.org/packages/d0/11/00341177ae71c6f5159a08168bcb98c6e6d196d372c94511f9f6c9afe0c6/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", size = 141285 }, + { url = "https://files.pythonhosted.org/packages/01/09/11d684ea5819e5a8f5100fb0b38cf8d02b514746607934134d31233e02c8/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", size = 151449 }, + { url = "https://files.pythonhosted.org/packages/08/06/9f5a12939db324d905dc1f70591ae7d7898d030d7662f0d426e2286f68c9/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", size = 143892 }, + { url = "https://files.pythonhosted.org/packages/93/62/5e89cdfe04584cb7f4d36003ffa2936681b03ecc0754f8e969c2becb7e24/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", size = 146123 }, + { url = "https://files.pythonhosted.org/packages/a9/ac/ab729a15c516da2ab70a05f8722ecfccc3f04ed7a18e45c75bbbaa347d61/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", size = 147943 }, + { url = "https://files.pythonhosted.org/packages/03/d2/3f392f23f042615689456e9a274640c1d2e5dd1d52de36ab8f7955f8f050/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", size = 142063 }, + { url = "https://files.pythonhosted.org/packages/f2/e3/e20aae5e1039a2cd9b08d9205f52142329f887f8cf70da3650326670bddf/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", size = 150578 }, + { url = "https://files.pythonhosted.org/packages/8d/af/779ad72a4da0aed925e1139d458adc486e61076d7ecdcc09e610ea8678db/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", size = 153629 }, + { url = "https://files.pythonhosted.org/packages/c2/b6/7aa450b278e7aa92cf7732140bfd8be21f5f29d5bf334ae987c945276639/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", size = 150778 }, + { url = "https://files.pythonhosted.org/packages/39/f4/d9f4f712d0951dcbfd42920d3db81b00dd23b6ab520419626f4023334056/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", size = 146453 }, + { url = "https://files.pythonhosted.org/packages/49/2b/999d0314e4ee0cff3cb83e6bc9aeddd397eeed693edb4facb901eb8fbb69/charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", size = 95479 }, + { url = "https://files.pythonhosted.org/packages/2d/ce/3cbed41cff67e455a386fb5e5dd8906cdda2ed92fbc6297921f2e4419309/charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", size = 102790 }, + { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995 }, + { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471 }, + { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831 }, + { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335 }, + { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862 }, + { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673 }, + { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211 }, + { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039 }, + { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939 }, + { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075 }, + { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340 }, + { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205 }, + { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441 }, + { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 }, + { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 }, + { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 }, + { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 }, + { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 }, + { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 }, + { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 }, + { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 }, + { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 }, + { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 }, + { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 }, + { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 }, + { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 }, + { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, +] + +[[package]] +name = "click-default-group" +version = "1.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/ce/edb087fb53de63dad3b36408ca30368f438738098e668b78c87f93cd41df/click_default_group-1.2.4.tar.gz", hash = "sha256:eb3f3c99ec0d456ca6cd2a7f08f7d4e91771bef51b01bdd9580cc6450fe1251e", size = 3505 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/1a/aff8bb287a4b1400f69e09a53bd65de96aa5cee5691925b38731c67fc695/click_default_group-1.2.4-py2.py3-none-any.whl", hash = "sha256:9b60486923720e7fc61731bdb32b617039aba820e22e1c88766b1125592eaa5f", size = 4123 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "coloredlogs" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "humanfriendly" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018 }, +] + +[[package]] +name = "configargparse" +version = "1.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/8a/73f1008adfad01cb923255b924b1528727b8270e67cb4ef41eabdc7d783e/ConfigArgParse-1.7.tar.gz", hash = "sha256:e7067471884de5478c58a511e529f0f9bd1c66bfef1dea90935438d6c23306d1", size = 43817 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/b3/b4ac838711fd74a2b4e6f746703cf9dd2cf5462d17dac07e349234e21b97/ConfigArgParse-1.7-py3-none-any.whl", hash = "sha256:d249da6591465c6c26df64a9f73d2536e743be2f244eb3ebe61114af2f94f86b", size = 25489 }, +] + +[[package]] +name = "coverage" +version = "7.6.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/d6/2b53ab3ee99f2262e6f0b8369a43f6d66658eab45510331c0b3d5c8c4272/coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2", size = 805941 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/67/81dc41ec8f548c365d04a29f1afd492d3176b372c33e47fa2a45a01dc13a/coverage-7.6.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:704c8c8c6ce6569286ae9622e534b4f5b9759b6f2cd643f1c1a61f666d534fe8", size = 208345 }, + { url = "https://files.pythonhosted.org/packages/33/43/17f71676016c8829bde69e24c852fef6bd9ed39f774a245d9ec98f689fa0/coverage-7.6.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ad7525bf0241e5502168ae9c643a2f6c219fa0a283001cee4cf23a9b7da75879", size = 208775 }, + { url = "https://files.pythonhosted.org/packages/86/25/c6ff0775f8960e8c0840845b723eed978d22a3cd9babd2b996e4a7c502c6/coverage-7.6.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06097c7abfa611c91edb9e6920264e5be1d6ceb374efb4986f38b09eed4cb2fe", size = 237925 }, + { url = "https://files.pythonhosted.org/packages/b0/3d/5f5bd37046243cb9d15fff2c69e498c2f4fe4f9b42a96018d4579ed3506f/coverage-7.6.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:220fa6c0ad7d9caef57f2c8771918324563ef0d8272c94974717c3909664e674", size = 235835 }, + { url = "https://files.pythonhosted.org/packages/b5/f1/9e6b75531fe33490b910d251b0bf709142e73a40e4e38a3899e6986fe088/coverage-7.6.12-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3688b99604a24492bcfe1c106278c45586eb819bf66a654d8a9a1433022fb2eb", size = 236966 }, + { url = "https://files.pythonhosted.org/packages/4f/bc/aef5a98f9133851bd1aacf130e754063719345d2fb776a117d5a8d516971/coverage-7.6.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d1a987778b9c71da2fc8948e6f2656da6ef68f59298b7e9786849634c35d2c3c", size = 236080 }, + { url = "https://files.pythonhosted.org/packages/eb/d0/56b4ab77f9b12aea4d4c11dc11cdcaa7c29130b837eb610639cf3400c9c3/coverage-7.6.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cec6b9ce3bd2b7853d4a4563801292bfee40b030c05a3d29555fd2a8ee9bd68c", size = 234393 }, + { url = "https://files.pythonhosted.org/packages/0d/77/28ef95c5d23fe3dd191a0b7d89c82fea2c2d904aef9315daf7c890e96557/coverage-7.6.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ace9048de91293e467b44bce0f0381345078389814ff6e18dbac8fdbf896360e", size = 235536 }, + { url = "https://files.pythonhosted.org/packages/29/62/18791d3632ee3ff3f95bc8599115707d05229c72db9539f208bb878a3d88/coverage-7.6.12-cp310-cp310-win32.whl", hash = "sha256:ea31689f05043d520113e0552f039603c4dd71fa4c287b64cb3606140c66f425", size = 211063 }, + { url = "https://files.pythonhosted.org/packages/fc/57/b3878006cedfd573c963e5c751b8587154eb10a61cc0f47a84f85c88a355/coverage-7.6.12-cp310-cp310-win_amd64.whl", hash = "sha256:676f92141e3c5492d2a1596d52287d0d963df21bf5e55c8b03075a60e1ddf8aa", size = 211955 }, + { url = "https://files.pythonhosted.org/packages/64/2d/da78abbfff98468c91fd63a73cccdfa0e99051676ded8dd36123e3a2d4d5/coverage-7.6.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e18aafdfb3e9ec0d261c942d35bd7c28d031c5855dadb491d2723ba54f4c3015", size = 208464 }, + { url = "https://files.pythonhosted.org/packages/31/f2/c269f46c470bdabe83a69e860c80a82e5e76840e9f4bbd7f38f8cebbee2f/coverage-7.6.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66fe626fd7aa5982cdebad23e49e78ef7dbb3e3c2a5960a2b53632f1f703ea45", size = 208893 }, + { url = "https://files.pythonhosted.org/packages/47/63/5682bf14d2ce20819998a49c0deadb81e608a59eed64d6bc2191bc8046b9/coverage-7.6.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ef01d70198431719af0b1f5dcbefc557d44a190e749004042927b2a3fed0702", size = 241545 }, + { url = "https://files.pythonhosted.org/packages/6a/b6/6b6631f1172d437e11067e1c2edfdb7238b65dff965a12bce3b6d1bf2be2/coverage-7.6.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e92ae5a289a4bc4c0aae710c0948d3c7892e20fd3588224ebe242039573bf0", size = 239230 }, + { url = "https://files.pythonhosted.org/packages/c7/01/9cd06cbb1be53e837e16f1b4309f6357e2dfcbdab0dd7cd3b1a50589e4e1/coverage-7.6.12-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e695df2c58ce526eeab11a2e915448d3eb76f75dffe338ea613c1201b33bab2f", size = 241013 }, + { url = "https://files.pythonhosted.org/packages/4b/26/56afefc03c30871326e3d99709a70d327ac1f33da383cba108c79bd71563/coverage-7.6.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d74c08e9aaef995f8c4ef6d202dbd219c318450fe2a76da624f2ebb9c8ec5d9f", size = 239750 }, + { url = "https://files.pythonhosted.org/packages/dd/ea/88a1ff951ed288f56aa561558ebe380107cf9132facd0b50bced63ba7238/coverage-7.6.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e995b3b76ccedc27fe4f477b349b7d64597e53a43fc2961db9d3fbace085d69d", size = 238462 }, + { url = "https://files.pythonhosted.org/packages/6e/d4/1d9404566f553728889409eff82151d515fbb46dc92cbd13b5337fa0de8c/coverage-7.6.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b1f097878d74fe51e1ddd1be62d8e3682748875b461232cf4b52ddc6e6db0bba", size = 239307 }, + { url = "https://files.pythonhosted.org/packages/12/c1/e453d3b794cde1e232ee8ac1d194fde8e2ba329c18bbf1b93f6f5eef606b/coverage-7.6.12-cp311-cp311-win32.whl", hash = "sha256:1f7ffa05da41754e20512202c866d0ebfc440bba3b0ed15133070e20bf5aeb5f", size = 211117 }, + { url = "https://files.pythonhosted.org/packages/d5/db/829185120c1686fa297294f8fcd23e0422f71070bf85ef1cc1a72ecb2930/coverage-7.6.12-cp311-cp311-win_amd64.whl", hash = "sha256:e216c5c45f89ef8971373fd1c5d8d1164b81f7f5f06bbf23c37e7908d19e8558", size = 212019 }, + { url = "https://files.pythonhosted.org/packages/e2/7f/4af2ed1d06ce6bee7eafc03b2ef748b14132b0bdae04388e451e4b2c529b/coverage-7.6.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b172f8e030e8ef247b3104902cc671e20df80163b60a203653150d2fc204d1ad", size = 208645 }, + { url = "https://files.pythonhosted.org/packages/dc/60/d19df912989117caa95123524d26fc973f56dc14aecdec5ccd7d0084e131/coverage-7.6.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:641dfe0ab73deb7069fb972d4d9725bf11c239c309ce694dd50b1473c0f641c3", size = 208898 }, + { url = "https://files.pythonhosted.org/packages/bd/10/fecabcf438ba676f706bf90186ccf6ff9f6158cc494286965c76e58742fa/coverage-7.6.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e549f54ac5f301e8e04c569dfdb907f7be71b06b88b5063ce9d6953d2d58574", size = 242987 }, + { url = "https://files.pythonhosted.org/packages/4c/53/4e208440389e8ea936f5f2b0762dcd4cb03281a7722def8e2bf9dc9c3d68/coverage-7.6.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:959244a17184515f8c52dcb65fb662808767c0bd233c1d8a166e7cf74c9ea985", size = 239881 }, + { url = "https://files.pythonhosted.org/packages/c4/47/2ba744af8d2f0caa1f17e7746147e34dfc5f811fb65fc153153722d58835/coverage-7.6.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bda1c5f347550c359f841d6614fb8ca42ae5cb0b74d39f8a1e204815ebe25750", size = 242142 }, + { url = "https://files.pythonhosted.org/packages/e9/90/df726af8ee74d92ee7e3bf113bf101ea4315d71508952bd21abc3fae471e/coverage-7.6.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ceeb90c3eda1f2d8c4c578c14167dbd8c674ecd7d38e45647543f19839dd6ea", size = 241437 }, + { url = "https://files.pythonhosted.org/packages/f6/af/995263fd04ae5f9cf12521150295bf03b6ba940d0aea97953bb4a6db3e2b/coverage-7.6.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f16f44025c06792e0fb09571ae454bcc7a3ec75eeb3c36b025eccf501b1a4c3", size = 239724 }, + { url = "https://files.pythonhosted.org/packages/1c/8e/5bb04f0318805e190984c6ce106b4c3968a9562a400180e549855d8211bd/coverage-7.6.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b076e625396e787448d27a411aefff867db2bffac8ed04e8f7056b07024eed5a", size = 241329 }, + { url = "https://files.pythonhosted.org/packages/9e/9d/fa04d9e6c3f6459f4e0b231925277cfc33d72dfab7fa19c312c03e59da99/coverage-7.6.12-cp312-cp312-win32.whl", hash = "sha256:00b2086892cf06c7c2d74983c9595dc511acca00665480b3ddff749ec4fb2a95", size = 211289 }, + { url = "https://files.pythonhosted.org/packages/53/40/53c7ffe3c0c3fff4d708bc99e65f3d78c129110d6629736faf2dbd60ad57/coverage-7.6.12-cp312-cp312-win_amd64.whl", hash = "sha256:7ae6eabf519bc7871ce117fb18bf14e0e343eeb96c377667e3e5dd12095e0288", size = 212079 }, + { url = "https://files.pythonhosted.org/packages/7a/7f/05818c62c7afe75df11e0233bd670948d68b36cdbf2a339a095bc02624a8/coverage-7.6.12-pp39.pp310-none-any.whl", hash = "sha256:7e39e845c4d764208e7b8f6a21c541ade741e2c41afabdfa1caa28687a3c98cf", size = 200558 }, + { url = "https://files.pythonhosted.org/packages/fb/b2/f655700e1024dec98b10ebaafd0cedbc25e40e4abe62a3c8e2ceef4f8f0a/coverage-7.6.12-py3-none-any.whl", hash = "sha256:eb8668cfbc279a536c633137deeb9435d2962caec279c3f8cf8b91fff6ff8953", size = 200552 }, +] + +[[package]] +name = "cryptography" +version = "44.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/67/545c79fe50f7af51dbad56d16b23fe33f63ee6a5d956b3cb68ea110cbe64/cryptography-44.0.1.tar.gz", hash = "sha256:f51f5705ab27898afda1aaa430f34ad90dc117421057782022edf0600bec5f14", size = 710819 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/27/5e3524053b4c8889da65cf7814a9d0d8514a05194a25e1e34f46852ee6eb/cryptography-44.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf688f615c29bfe9dfc44312ca470989279f0e94bb9f631f85e3459af8efc009", size = 6642022 }, + { url = "https://files.pythonhosted.org/packages/34/b9/4d1fa8d73ae6ec350012f89c3abfbff19fc95fe5420cf972e12a8d182986/cryptography-44.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd7c7e2d71d908dc0f8d2027e1604102140d84b155e658c20e8ad1304317691f", size = 3943865 }, + { url = "https://files.pythonhosted.org/packages/6e/57/371a9f3f3a4500807b5fcd29fec77f418ba27ffc629d88597d0d1049696e/cryptography-44.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:887143b9ff6bad2b7570da75a7fe8bbf5f65276365ac259a5d2d5147a73775f2", size = 4162562 }, + { url = "https://files.pythonhosted.org/packages/c5/1d/5b77815e7d9cf1e3166988647f336f87d5634a5ccecec2ffbe08ef8dd481/cryptography-44.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:322eb03ecc62784536bc173f1483e76747aafeb69c8728df48537eb431cd1911", size = 3951923 }, + { url = "https://files.pythonhosted.org/packages/28/01/604508cd34a4024467cd4105887cf27da128cba3edd435b54e2395064bfb/cryptography-44.0.1-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:21377472ca4ada2906bc313168c9dc7b1d7ca417b63c1c3011d0c74b7de9ae69", size = 3685194 }, + { url = "https://files.pythonhosted.org/packages/c6/3d/d3c55d4f1d24580a236a6753902ef6d8aafd04da942a1ee9efb9dc8fd0cb/cryptography-44.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:df978682c1504fc93b3209de21aeabf2375cb1571d4e61907b3e7a2540e83026", size = 4187790 }, + { url = "https://files.pythonhosted.org/packages/ea/a6/44d63950c8588bfa8594fd234d3d46e93c3841b8e84a066649c566afb972/cryptography-44.0.1-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:eb3889330f2a4a148abead555399ec9a32b13b7c8ba969b72d8e500eb7ef84cd", size = 3951343 }, + { url = "https://files.pythonhosted.org/packages/c1/17/f5282661b57301204cbf188254c1a0267dbd8b18f76337f0a7ce1038888c/cryptography-44.0.1-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:8e6a85a93d0642bd774460a86513c5d9d80b5c002ca9693e63f6e540f1815ed0", size = 4187127 }, + { url = "https://files.pythonhosted.org/packages/f3/68/abbae29ed4f9d96596687f3ceea8e233f65c9645fbbec68adb7c756bb85a/cryptography-44.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6f76fdd6fd048576a04c5210d53aa04ca34d2ed63336d4abd306d0cbe298fddf", size = 4070666 }, + { url = "https://files.pythonhosted.org/packages/0f/10/cf91691064a9e0a88ae27e31779200b1505d3aee877dbe1e4e0d73b4f155/cryptography-44.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6c8acf6f3d1f47acb2248ec3ea261171a671f3d9428e34ad0357148d492c7864", size = 4288811 }, + { url = "https://files.pythonhosted.org/packages/38/78/74ea9eb547d13c34e984e07ec8a473eb55b19c1451fe7fc8077c6a4b0548/cryptography-44.0.1-cp37-abi3-win32.whl", hash = "sha256:24979e9f2040c953a94bf3c6782e67795a4c260734e5264dceea65c8f4bae64a", size = 2771882 }, + { url = "https://files.pythonhosted.org/packages/cf/6c/3907271ee485679e15c9f5e93eac6aa318f859b0aed8d369afd636fafa87/cryptography-44.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:fd0ee90072861e276b0ff08bd627abec29e32a53b2be44e41dbcdf87cbee2b00", size = 3206989 }, + { url = "https://files.pythonhosted.org/packages/9f/f1/676e69c56a9be9fd1bffa9bc3492366901f6e1f8f4079428b05f1414e65c/cryptography-44.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a2d8a7045e1ab9b9f803f0d9531ead85f90c5f2859e653b61497228b18452008", size = 6643714 }, + { url = "https://files.pythonhosted.org/packages/ba/9f/1775600eb69e72d8f9931a104120f2667107a0ee478f6ad4fe4001559345/cryptography-44.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8272f257cf1cbd3f2e120f14c68bff2b6bdfcc157fafdee84a1b795efd72862", size = 3943269 }, + { url = "https://files.pythonhosted.org/packages/25/ba/e00d5ad6b58183829615be7f11f55a7b6baa5a06910faabdc9961527ba44/cryptography-44.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e8d181e90a777b63f3f0caa836844a1182f1f265687fac2115fcf245f5fbec3", size = 4166461 }, + { url = "https://files.pythonhosted.org/packages/b3/45/690a02c748d719a95ab08b6e4decb9d81e0ec1bac510358f61624c86e8a3/cryptography-44.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:436df4f203482f41aad60ed1813811ac4ab102765ecae7a2bbb1dbb66dcff5a7", size = 3950314 }, + { url = "https://files.pythonhosted.org/packages/e6/50/bf8d090911347f9b75adc20f6f6569ed6ca9b9bff552e6e390f53c2a1233/cryptography-44.0.1-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4f422e8c6a28cf8b7f883eb790695d6d45b0c385a2583073f3cec434cc705e1a", size = 3686675 }, + { url = "https://files.pythonhosted.org/packages/e1/e7/cfb18011821cc5f9b21efb3f94f3241e3a658d267a3bf3a0f45543858ed8/cryptography-44.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:72198e2b5925155497a5a3e8c216c7fb3e64c16ccee11f0e7da272fa93b35c4c", size = 4190429 }, + { url = "https://files.pythonhosted.org/packages/07/ef/77c74d94a8bfc1a8a47b3cafe54af3db537f081742ee7a8a9bd982b62774/cryptography-44.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a46a89ad3e6176223b632056f321bc7de36b9f9b93b2cc1cccf935a3849dc62", size = 3950039 }, + { url = "https://files.pythonhosted.org/packages/6d/b9/8be0ff57c4592382b77406269b1e15650c9f1a167f9e34941b8515b97159/cryptography-44.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:53f23339864b617a3dfc2b0ac8d5c432625c80014c25caac9082314e9de56f41", size = 4189713 }, + { url = "https://files.pythonhosted.org/packages/78/e1/4b6ac5f4100545513b0847a4d276fe3c7ce0eacfa73e3b5ebd31776816ee/cryptography-44.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:888fcc3fce0c888785a4876ca55f9f43787f4c5c1cc1e2e0da71ad481ff82c5b", size = 4071193 }, + { url = "https://files.pythonhosted.org/packages/3d/cb/afff48ceaed15531eab70445abe500f07f8f96af2bb35d98af6bfa89ebd4/cryptography-44.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:00918d859aa4e57db8299607086f793fa7813ae2ff5a4637e318a25ef82730f7", size = 4289566 }, + { url = "https://files.pythonhosted.org/packages/30/6f/4eca9e2e0f13ae459acd1ca7d9f0257ab86e68f44304847610afcb813dc9/cryptography-44.0.1-cp39-abi3-win32.whl", hash = "sha256:9b336599e2cb77b1008cb2ac264b290803ec5e8e89d618a5e978ff5eb6f715d9", size = 2772371 }, + { url = "https://files.pythonhosted.org/packages/d2/05/5533d30f53f10239616a357f080892026db2d550a40c393d0a8a7af834a9/cryptography-44.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:e403f7f766ded778ecdb790da786b418a9f2394f36e8cc8b796cc056ab05f44f", size = 3207303 }, + { url = "https://files.pythonhosted.org/packages/15/06/507bfb5c7e048114a0185dd65f7814677a2ba285d15705c3d69e660c21d7/cryptography-44.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1f9a92144fa0c877117e9748c74501bea842f93d21ee00b0cf922846d9d0b183", size = 3380782 }, + { url = "https://files.pythonhosted.org/packages/e0/f1/7fb4982d59aa86e1a116c812b545e7fc045352be07738ae3fb278835a9a4/cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:610a83540765a8d8ce0f351ce42e26e53e1f774a6efb71eb1b41eb01d01c3d12", size = 3888155 }, + { url = "https://files.pythonhosted.org/packages/60/7b/cbc203838d3092203493d18b923fbbb1de64e0530b332a713ba376905b0b/cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5fed5cd6102bb4eb843e3315d2bf25fede494509bddadb81e03a859c1bc17b83", size = 4106417 }, + { url = "https://files.pythonhosted.org/packages/12/c7/2fe59fb085ab418acc82e91e040a6acaa7b1696fcc1c1055317537fbf0d3/cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:f4daefc971c2d1f82f03097dc6f216744a6cd2ac0f04c68fb935ea2ba2a0d420", size = 3887540 }, + { url = "https://files.pythonhosted.org/packages/48/89/09fc7b115f60f5bd970b80e32244f8e9aeeb9244bf870b63420cec3b5cd5/cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94f99f2b943b354a5b6307d7e8d19f5c423a794462bde2bf310c770ba052b1c4", size = 4106040 }, + { url = "https://files.pythonhosted.org/packages/2e/38/3fd83c4690dc7d753a442a284b3826ea5e5c380a411443c66421cd823898/cryptography-44.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d9c5b9f698a83c8bd71e0f4d3f9f839ef244798e5ffe96febfa9714717db7af7", size = 3134657 }, +] + +[[package]] +name = "distlib" +version = "0.3.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277 }, +] + +[[package]] +name = "docformatter" +version = "1.7.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "charset-normalizer" }, + { name = "untokenize" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/44/aba2c40cf796121b35835ea8c00bc5d93f2f70730eca53b36b8bbbfaefe1/docformatter-1.7.5.tar.gz", hash = "sha256:ffed3da0daffa2e77f80ccba4f0e50bfa2755e1c10e130102571c890a61b246e", size = 25893 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/95/568a2fca29df365b82012b09b64964a05f4f20ac83c2137b262f3fa3188f/docformatter-1.7.5-py3-none-any.whl", hash = "sha256:a24f5545ed1f30af00d106f5d85dc2fce4959295687c24c8f39f5263afaf9186", size = 32798 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, +] + +[[package]] +name = "filelock" +version = "3.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/9c/0b15fb47b464e1b663b1acd1253a062aa5feecb07d4e597daea542ebd2b5/filelock-3.17.0.tar.gz", hash = "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e", size = 18027 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/ec/00d68c4ddfedfe64159999e5f8a98fb8442729a63e2077eb9dcd89623d27/filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338", size = 16164 }, +] + +[[package]] +name = "flatbuffers" +version = "25.2.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/30/eb5dce7994fc71a2f685d98ec33cc660c0a5887db5610137e60d8cbc4489/flatbuffers-25.2.10.tar.gz", hash = "sha256:97e451377a41262f8d9bd4295cc836133415cc03d8cb966410a4af92eb00d26e", size = 22170 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/25/155f9f080d5e4bc0082edfda032ea2bc2b8fab3f4d25d46c1e9dd22a1a89/flatbuffers-25.2.10-py2.py3-none-any.whl", hash = "sha256:ebba5f4d5ea615af3f7fd70fc310636fbb2bbd1f566ac0a23d98dd412de50051", size = 30953 }, +] + +[[package]] +name = "fonttools" +version = "4.56.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/8c/9ffa2a555af0e5e5d0e2ed7fdd8c9bef474ed676995bb4c57c9cd0014248/fonttools-4.56.0.tar.gz", hash = "sha256:a114d1567e1a1586b7e9e7fc2ff686ca542a82769a296cef131e4c4af51e58f4", size = 3462892 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/6ac30c2cc6a29454260f13c9c6422fc509b7982c13cd4597041260d8f482/fonttools-4.56.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:331954d002dbf5e704c7f3756028e21db07097c19722569983ba4d74df014000", size = 2752190 }, + { url = "https://files.pythonhosted.org/packages/92/3a/ac382a8396d1b420ee45eeb0f65b614a9ca7abbb23a1b17524054f0f2200/fonttools-4.56.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8d1613abd5af2f93c05867b3a3759a56e8bf97eb79b1da76b2bc10892f96ff16", size = 2280624 }, + { url = "https://files.pythonhosted.org/packages/8a/ae/00b58bfe20e9ff7fbc3dda38f5d127913942b5e252288ea9583099a31bf5/fonttools-4.56.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:705837eae384fe21cee5e5746fd4f4b2f06f87544fa60f60740007e0aa600311", size = 4562074 }, + { url = "https://files.pythonhosted.org/packages/46/d0/0004ca8f6a200252e5bd6982ed99b5fe58c4c59efaf5f516621c4cd8f703/fonttools-4.56.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc871904a53a9d4d908673c6faa15689874af1c7c5ac403a8e12d967ebd0c0dc", size = 4604747 }, + { url = "https://files.pythonhosted.org/packages/45/ea/c8862bd3e09d143ef8ed8268ec8a7d477828f960954889e65288ac050b08/fonttools-4.56.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:38b947de71748bab150259ee05a775e8a0635891568e9fdb3cdd7d0e0004e62f", size = 4559025 }, + { url = "https://files.pythonhosted.org/packages/8f/75/bb88a9552ec1de31a414066257bfd9f40f4ada00074f7a3799ea39b5741f/fonttools-4.56.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:86b2a1013ef7a64d2e94606632683f07712045ed86d937c11ef4dde97319c086", size = 4728482 }, + { url = "https://files.pythonhosted.org/packages/2a/5f/80a2b640df1e1bb7d459d62c8b3f37fe83fd413897e549106d4ebe6371f5/fonttools-4.56.0-cp310-cp310-win32.whl", hash = "sha256:133bedb9a5c6376ad43e6518b7e2cd2f866a05b1998f14842631d5feb36b5786", size = 2155557 }, + { url = "https://files.pythonhosted.org/packages/8f/85/0904f9dbe51ac70d878d3242a8583b9453a09105c3ed19c6301247fd0d3a/fonttools-4.56.0-cp310-cp310-win_amd64.whl", hash = "sha256:17f39313b649037f6c800209984a11fc256a6137cbe5487091c6c7187cae4685", size = 2200017 }, + { url = "https://files.pythonhosted.org/packages/35/56/a2f3e777d48fcae7ecd29de4d96352d84e5ea9871e5f3fc88241521572cf/fonttools-4.56.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ef04bc7827adb7532be3d14462390dd71287644516af3f1e67f1e6ff9c6d6df", size = 2753325 }, + { url = "https://files.pythonhosted.org/packages/71/85/d483e9c4e5ed586b183bf037a353e8d766366b54fd15519b30e6178a6a6e/fonttools-4.56.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ffda9b8cd9cb8b301cae2602ec62375b59e2e2108a117746f12215145e3f786c", size = 2281554 }, + { url = "https://files.pythonhosted.org/packages/09/67/060473b832b2fade03c127019794df6dc02d9bc66fa4210b8e0d8a99d1e5/fonttools-4.56.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e993e8db36306cc3f1734edc8ea67906c55f98683d6fd34c3fc5593fdbba4c", size = 4869260 }, + { url = "https://files.pythonhosted.org/packages/28/e9/47c02d5a7027e8ed841ab6a10ca00c93dadd5f16742f1af1fa3f9978adf4/fonttools-4.56.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:003548eadd674175510773f73fb2060bb46adb77c94854af3e0cc5bc70260049", size = 4898508 }, + { url = "https://files.pythonhosted.org/packages/bf/8a/221d456d1afb8ca043cfd078f59f187ee5d0a580f4b49351b9ce95121f57/fonttools-4.56.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd9825822e7bb243f285013e653f6741954d8147427aaa0324a862cdbf4cbf62", size = 4877700 }, + { url = "https://files.pythonhosted.org/packages/a4/8c/e503863adf7a6aeff7b960e2f66fa44dd0c29a7a8b79765b2821950d7b05/fonttools-4.56.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b23d30a2c0b992fb1c4f8ac9bfde44b5586d23457759b6cf9a787f1a35179ee0", size = 5045817 }, + { url = "https://files.pythonhosted.org/packages/2b/50/79ba3b7e42f4eaa70b82b9e79155f0f6797858dc8a97862428b6852c6aee/fonttools-4.56.0-cp311-cp311-win32.whl", hash = "sha256:47b5e4680002ae1756d3ae3b6114e20aaee6cc5c69d1e5911f5ffffd3ee46c6b", size = 2154426 }, + { url = "https://files.pythonhosted.org/packages/3b/90/4926e653041c4116ecd43e50e3c79f5daae6dcafc58ceb64bc4f71dd4924/fonttools-4.56.0-cp311-cp311-win_amd64.whl", hash = "sha256:14a3e3e6b211660db54ca1ef7006401e4a694e53ffd4553ab9bc87ead01d0f05", size = 2200937 }, + { url = "https://files.pythonhosted.org/packages/39/32/71cfd6877999576a11824a7fe7bc0bb57c5c72b1f4536fa56a3e39552643/fonttools-4.56.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6f195c14c01bd057bc9b4f70756b510e009c83c5ea67b25ced3e2c38e6ee6e9", size = 2747757 }, + { url = "https://files.pythonhosted.org/packages/15/52/d9f716b072c5061a0b915dd4c387f74bef44c68c069e2195c753905bd9b7/fonttools-4.56.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fa760e5fe8b50cbc2d71884a1eff2ed2b95a005f02dda2fa431560db0ddd927f", size = 2279007 }, + { url = "https://files.pythonhosted.org/packages/d1/97/f1b3a8afa9a0d814a092a25cd42f59ccb98a0bb7a295e6e02fc9ba744214/fonttools-4.56.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d54a45d30251f1d729e69e5b675f9a08b7da413391a1227781e2a297fa37f6d2", size = 4783991 }, + { url = "https://files.pythonhosted.org/packages/95/70/2a781bedc1c45a0c61d29c56425609b22ed7f971da5d7e5df2679488741b/fonttools-4.56.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:661a8995d11e6e4914a44ca7d52d1286e2d9b154f685a4d1f69add8418961563", size = 4855109 }, + { url = "https://files.pythonhosted.org/packages/0c/02/a2597858e61a5e3fb6a14d5f6be9e6eb4eaf090da56ad70cedcbdd201685/fonttools-4.56.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9d94449ad0a5f2a8bf5d2f8d71d65088aee48adbe45f3c5f8e00e3ad861ed81a", size = 4762496 }, + { url = "https://files.pythonhosted.org/packages/f2/00/aaf00100d6078fdc73f7352b44589804af9dc12b182a2540b16002152ba4/fonttools-4.56.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f59746f7953f69cc3290ce2f971ab01056e55ddd0fb8b792c31a8acd7fee2d28", size = 4990094 }, + { url = "https://files.pythonhosted.org/packages/bf/dc/3ff1db522460db60cf3adaf1b64e0c72b43406717d139786d3fa1eb20709/fonttools-4.56.0-cp312-cp312-win32.whl", hash = "sha256:bce60f9a977c9d3d51de475af3f3581d9b36952e1f8fc19a1f2254f1dda7ce9c", size = 2142888 }, + { url = "https://files.pythonhosted.org/packages/6f/e3/5a181a85777f7809076e51f7422e0dc77eb04676c40ec8bf6a49d390d1ff/fonttools-4.56.0-cp312-cp312-win_amd64.whl", hash = "sha256:300c310bb725b2bdb4f5fc7e148e190bd69f01925c7ab437b9c0ca3e1c7cd9ba", size = 2189734 }, + { url = "https://files.pythonhosted.org/packages/bf/ff/44934a031ce5a39125415eb405b9efb76fe7f9586b75291d66ae5cbfc4e6/fonttools-4.56.0-py3-none-any.whl", hash = "sha256:1088182f68c303b50ca4dc0c82d42083d176cba37af1937e1a976a31149d4d14", size = 1089800 }, +] + +[[package]] +name = "fsspec" +version = "2025.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/79/68612ed99700e6413de42895aa725463e821a6b3be75c87fcce1b4af4c70/fsspec-2025.2.0.tar.gz", hash = "sha256:1c24b16eaa0a1798afa0337aa0db9b256718ab2a89c425371f5628d22c3b6afd", size = 292283 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/94/758680531a00d06e471ef649e4ec2ed6bf185356a7f9fbfbb7368a40bd49/fsspec-2025.2.0-py3-none-any.whl", hash = "sha256:9de2ad9ce1f85e1931858535bc882543171d197001a0a5eb2ddc04f1781ab95b", size = 184484 }, +] + +[[package]] +name = "git-cliff" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/2c/4be68bbdea827ad2b3fd5e7ae880da65e5e4dae8a37af368cc6a1a6ec1ab/git_cliff-2.8.0.tar.gz", hash = "sha256:ab252f0d31c6febb57b6d4f24f9584b779327af43bc94e2bdb00867248cb5d0d", size = 87078 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/48/06035b44dee6e1bdaea093d48d8b32f4570c665366accf6143e60faa92f4/git_cliff-2.8.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:bfd552f1bda845e85e0a570336b4dacaf76e481f9ebe00301fe5c869377e2289", size = 6325036 }, + { url = "https://files.pythonhosted.org/packages/e3/90/eaa5f850a5d14132026d121896751424e0249e14368e339272f05edec4b9/git_cliff-2.8.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:281504bb22da43b671f9131c3cd7f0d56fa4aa7c9a1922948f9a537a76b38ea7", size = 5870796 }, + { url = "https://files.pythonhosted.org/packages/8b/50/79d7202ce0ca3839b5a03e334201ef58c285e2be5034f9a6eb84a15644d3/git_cliff-2.8.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:485b825fec2600bd2ab847cd5f1724c874ddc0aed7e57d7fb43e484fcc114f8a", size = 6226344 }, + { url = "https://files.pythonhosted.org/packages/4d/d8/cf470a17daeef8e562b3a08310e6d35a00e79ae83391f690006455f10da8/git_cliff-2.8.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dad66722d0df40bd3722cf274cae5f6f55d61f5faa5974c48a68c62179592f7", size = 6669351 }, + { url = "https://files.pythonhosted.org/packages/f2/9d/74357c40bf6f10b91f4422f2ffb5b97384cb33bd01130b43608378740afc/git_cliff-2.8.0-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:96cc47b9716fdeddfa0d68f6c93d6f7deb056da3dcefc0b4e337edb463b5790d", size = 6276116 }, + { url = "https://files.pythonhosted.org/packages/10/7e/12d5c62dd79a8650a75a5d3f28535ae4b4fe5c4b2ae849850608e7cda981/git_cliff-2.8.0-py3-none-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aef4513c51ccd05f21cb539d4125902dbe555abd56695c9793f01636775075ee", size = 6473118 }, + { url = "https://files.pythonhosted.org/packages/20/a7/05285fddc01398b486f51ec799891c238889e680a4711322e55d4eee7723/git_cliff-2.8.0-py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:db21d6f3bf2641bd45340d076e108692606b723ddf8fc7be6bef3ffc9faedaff", size = 6929288 }, + { url = "https://files.pythonhosted.org/packages/48/9c/19e98f35fc27657fe08e22a1cbae8cf40138cb66959f213b112771f25a8d/git_cliff-2.8.0-py3-none-win32.whl", hash = "sha256:09611b9e909f2635378bf185616a82ec7e9bbccb6e79ae6ce204c2ea4005f215", size = 5963978 }, + { url = "https://files.pythonhosted.org/packages/75/e0/c4413fd66f0fb58109627afe64ac8574f46fd7358a4aabca8ed5e85d6a9c/git_cliff-2.8.0-py3-none-win_amd64.whl", hash = "sha256:e284e6b1e7f701b9f2836f31fec38c408da54f361f1a281d0e4709f33f00d905", size = 6716805 }, +] + +[[package]] +name = "gitdb" +version = "4.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794 }, +] + +[[package]] +name = "gitpython" +version = "3.1.44" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/89/37df0b71473153574a5cdef8f242de422a0f5d26d7a9e231e6f169b4ad14/gitpython-3.1.44.tar.gz", hash = "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269", size = 214196 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/9a/4114a9057db2f1462d5c8f8390ab7383925fe1ac012eaa42402ad65c2963/GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110", size = 207599 }, +] + +[[package]] +name = "h11" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, +] + +[[package]] +name = "httpcore" +version = "1.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, +] + +[package.optional-dependencies] +socks = [ + { name = "socksio" }, +] + +[[package]] +name = "huggingface-hub" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/ce/a734204aaae6c35a22f9956ebcd8d8708ae5b842e15d6f42bd6f49e634a4/huggingface_hub-0.28.1.tar.gz", hash = "sha256:893471090c98e3b6efbdfdacafe4052b20b84d59866fb6f54c33d9af18c303ae", size = 387074 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/da/6c2bea5327b640920267d3bf2c9fc114cfbd0a5de234d81cda80cc9e33c8/huggingface_hub-0.28.1-py3-none-any.whl", hash = "sha256:aa6b9a3ffdae939b72c464dbb0d7f99f56e649b55c3d52406f49e0a5a620c0a7", size = 464068 }, +] + +[[package]] +name = "humanfriendly" +version = "10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyreadline3", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794 }, +] + +[[package]] +name = "identify" +version = "2.6.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/d1/524aa3350f78bcd714d148ade6133d67d6b7de2cdbae7d99039c024c9a25/identify-2.6.7.tar.gz", hash = "sha256:3fa266b42eba321ee0b2bb0936a6a6b9e36a1351cbb69055b3082f4193035684", size = 99260 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/00/1fd4a117c6c93f2dcc5b7edaeaf53ea45332ef966429be566ca16c2beb94/identify-2.6.7-py2.py3-none-any.whl", hash = "sha256:155931cb617a401807b09ecec6635d6c692d180090a1cedca8ef7d58ba5b6aa0", size = 99097 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "jinja2" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/92/b3130cbbf5591acf9ade8708c365f3238046ac7cb8ccba6e81abccb0ccff/jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", size = 244674 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/0f/2ba5fbcd631e3e88689309dbe978c5769e883e4b84ebfe7da30b43275c5a/jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb", size = 134596 }, +] + +[[package]] +name = "jiter" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/70/90bc7bd3932e651486861df5c8ffea4ca7c77d28e8532ddefe2abc561a53/jiter-0.8.2.tar.gz", hash = "sha256:cd73d3e740666d0e639f678adb176fad25c1bcbdae88d8d7b857e1783bb4212d", size = 163007 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/f3/8c11e0e87bd5934c414f9b1cfae3cbfd4a938d4669d57cb427e1c4d11a7f/jiter-0.8.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ca8577f6a413abe29b079bc30f907894d7eb07a865c4df69475e868d73e71c7b", size = 303381 }, + { url = "https://files.pythonhosted.org/packages/ea/28/4cd3f0bcbf40e946bc6a62a82c951afc386a25673d3d8d5ee461f1559bbe/jiter-0.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b25bd626bde7fb51534190c7e3cb97cee89ee76b76d7585580e22f34f5e3f393", size = 311718 }, + { url = "https://files.pythonhosted.org/packages/0d/17/57acab00507e60bd954eaec0837d9d7b119b4117ff49b8a62f2b646f32ed/jiter-0.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5c826a221851a8dc028eb6d7d6429ba03184fa3c7e83ae01cd6d3bd1d4bd17d", size = 335465 }, + { url = "https://files.pythonhosted.org/packages/74/b9/1a3ddd2bc95ae17c815b021521020f40c60b32137730126bada962ef32b4/jiter-0.8.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d35c864c2dff13dfd79fb070fc4fc6235d7b9b359efe340e1261deb21b9fcb66", size = 355570 }, + { url = "https://files.pythonhosted.org/packages/78/69/6d29e2296a934199a7d0dde673ecccf98c9c8db44caf0248b3f2b65483cb/jiter-0.8.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f557c55bc2b7676e74d39d19bcb8775ca295c7a028246175d6a8b431e70835e5", size = 381383 }, + { url = "https://files.pythonhosted.org/packages/22/d7/fbc4c3fb1bf65f9be22a32759b539f88e897aeb13fe84ab0266e4423487a/jiter-0.8.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:580ccf358539153db147e40751a0b41688a5ceb275e6f3e93d91c9467f42b2e3", size = 390454 }, + { url = "https://files.pythonhosted.org/packages/4d/a0/3993cda2e267fe679b45d0bcc2cef0b4504b0aa810659cdae9737d6bace9/jiter-0.8.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af102d3372e917cffce49b521e4c32c497515119dc7bd8a75665e90a718bbf08", size = 345039 }, + { url = "https://files.pythonhosted.org/packages/b9/ef/69c18562b4c09ce88fab5df1dcaf643f6b1a8b970b65216e7221169b81c4/jiter-0.8.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cadcc978f82397d515bb2683fc0d50103acff2a180552654bb92d6045dec2c49", size = 376200 }, + { url = "https://files.pythonhosted.org/packages/4d/17/0b5a8de46a6ab4d836f70934036278b49b8530c292b29dde3483326d4555/jiter-0.8.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ba5bdf56969cad2019d4e8ffd3f879b5fdc792624129741d3d83fc832fef8c7d", size = 511158 }, + { url = "https://files.pythonhosted.org/packages/6c/b2/c401a0a2554b36c9e6d6e4876b43790d75139cf3936f0222e675cbc23451/jiter-0.8.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3b94a33a241bee9e34b8481cdcaa3d5c2116f575e0226e421bed3f7a6ea71cff", size = 503956 }, + { url = "https://files.pythonhosted.org/packages/d4/02/a0291ed7d72c0ac130f172354ee3cf0b2556b69584de391463a8ee534f40/jiter-0.8.2-cp310-cp310-win32.whl", hash = "sha256:6e5337bf454abddd91bd048ce0dca5134056fc99ca0205258766db35d0a2ea43", size = 202846 }, + { url = "https://files.pythonhosted.org/packages/ad/20/8c988831ae4bf437e29f1671e198fc99ba8fe49f2895f23789acad1d1811/jiter-0.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:4a9220497ca0cb1fe94e3f334f65b9b5102a0b8147646118f020d8ce1de70105", size = 204414 }, + { url = "https://files.pythonhosted.org/packages/cb/b0/c1a7caa7f9dc5f1f6cfa08722867790fe2d3645d6e7170ca280e6e52d163/jiter-0.8.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2dd61c5afc88a4fda7d8b2cf03ae5947c6ac7516d32b7a15bf4b49569a5c076b", size = 303666 }, + { url = "https://files.pythonhosted.org/packages/f5/97/0468bc9eeae43079aaa5feb9267964e496bf13133d469cfdc135498f8dd0/jiter-0.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a6c710d657c8d1d2adbbb5c0b0c6bfcec28fd35bd6b5f016395f9ac43e878a15", size = 311934 }, + { url = "https://files.pythonhosted.org/packages/e5/69/64058e18263d9a5f1e10f90c436853616d5f047d997c37c7b2df11b085ec/jiter-0.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9584de0cd306072635fe4b89742bf26feae858a0683b399ad0c2509011b9dc0", size = 335506 }, + { url = "https://files.pythonhosted.org/packages/9d/14/b747f9a77b8c0542141d77ca1e2a7523e854754af2c339ac89a8b66527d6/jiter-0.8.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5a90a923338531b7970abb063cfc087eebae6ef8ec8139762007188f6bc69a9f", size = 355849 }, + { url = "https://files.pythonhosted.org/packages/53/e2/98a08161db7cc9d0e39bc385415890928ff09709034982f48eccfca40733/jiter-0.8.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21974d246ed0181558087cd9f76e84e8321091ebfb3a93d4c341479a736f099", size = 381700 }, + { url = "https://files.pythonhosted.org/packages/7a/38/1674672954d35bce3b1c9af99d5849f9256ac8f5b672e020ac7821581206/jiter-0.8.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32475a42b2ea7b344069dc1e81445cfc00b9d0e3ca837f0523072432332e9f74", size = 389710 }, + { url = "https://files.pythonhosted.org/packages/f8/9b/92f9da9a9e107d019bcf883cd9125fa1690079f323f5a9d5c6986eeec3c0/jiter-0.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b9931fd36ee513c26b5bf08c940b0ac875de175341cbdd4fa3be109f0492586", size = 345553 }, + { url = "https://files.pythonhosted.org/packages/44/a6/6d030003394e9659cd0d7136bbeabd82e869849ceccddc34d40abbbbb269/jiter-0.8.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ce0820f4a3a59ddced7fce696d86a096d5cc48d32a4183483a17671a61edfddc", size = 376388 }, + { url = "https://files.pythonhosted.org/packages/ad/8d/87b09e648e4aca5f9af89e3ab3cfb93db2d1e633b2f2931ede8dabd9b19a/jiter-0.8.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8ffc86ae5e3e6a93765d49d1ab47b6075a9c978a2b3b80f0f32628f39caa0c88", size = 511226 }, + { url = "https://files.pythonhosted.org/packages/77/95/8008ebe4cdc82eac1c97864a8042ca7e383ed67e0ec17bfd03797045c727/jiter-0.8.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5127dc1abd809431172bc3fbe8168d6b90556a30bb10acd5ded41c3cfd6f43b6", size = 504134 }, + { url = "https://files.pythonhosted.org/packages/26/0d/3056a74de13e8b2562e4d526de6dac2f65d91ace63a8234deb9284a1d24d/jiter-0.8.2-cp311-cp311-win32.whl", hash = "sha256:66227a2c7b575720c1871c8800d3a0122bb8ee94edb43a5685aa9aceb2782d44", size = 203103 }, + { url = "https://files.pythonhosted.org/packages/4e/1e/7f96b798f356e531ffc0f53dd2f37185fac60fae4d6c612bbbd4639b90aa/jiter-0.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:cde031d8413842a1e7501e9129b8e676e62a657f8ec8166e18a70d94d4682855", size = 206717 }, + { url = "https://files.pythonhosted.org/packages/a1/17/c8747af8ea4e045f57d6cfd6fc180752cab9bc3de0e8a0c9ca4e8af333b1/jiter-0.8.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:e6ec2be506e7d6f9527dae9ff4b7f54e68ea44a0ef6b098256ddf895218a2f8f", size = 302027 }, + { url = "https://files.pythonhosted.org/packages/3c/c1/6da849640cd35a41e91085723b76acc818d4b7d92b0b6e5111736ce1dd10/jiter-0.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76e324da7b5da060287c54f2fabd3db5f76468006c811831f051942bf68c9d44", size = 310326 }, + { url = "https://files.pythonhosted.org/packages/06/99/a2bf660d8ccffee9ad7ed46b4f860d2108a148d0ea36043fd16f4dc37e94/jiter-0.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:180a8aea058f7535d1c84183c0362c710f4750bef66630c05f40c93c2b152a0f", size = 334242 }, + { url = "https://files.pythonhosted.org/packages/a7/5f/cea1c17864828731f11427b9d1ab7f24764dbd9aaf4648a7f851164d2718/jiter-0.8.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:025337859077b41548bdcbabe38698bcd93cfe10b06ff66617a48ff92c9aec60", size = 356654 }, + { url = "https://files.pythonhosted.org/packages/e9/13/62774b7e5e7f5d5043efe1d0f94ead66e6d0f894ae010adb56b3f788de71/jiter-0.8.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ecff0dc14f409599bbcafa7e470c00b80f17abc14d1405d38ab02e4b42e55b57", size = 379967 }, + { url = "https://files.pythonhosted.org/packages/ec/fb/096b34c553bb0bd3f2289d5013dcad6074948b8d55212aa13a10d44c5326/jiter-0.8.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ffd9fee7d0775ebaba131f7ca2e2d83839a62ad65e8e02fe2bd8fc975cedeb9e", size = 389252 }, + { url = "https://files.pythonhosted.org/packages/17/61/beea645c0bf398ced8b199e377b61eb999d8e46e053bb285c91c3d3eaab0/jiter-0.8.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14601dcac4889e0a1c75ccf6a0e4baf70dbc75041e51bcf8d0e9274519df6887", size = 345490 }, + { url = "https://files.pythonhosted.org/packages/d5/df/834aa17ad5dcc3cf0118821da0a0cf1589ea7db9832589278553640366bc/jiter-0.8.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:92249669925bc1c54fcd2ec73f70f2c1d6a817928480ee1c65af5f6b81cdf12d", size = 376991 }, + { url = "https://files.pythonhosted.org/packages/67/80/87d140399d382fb4ea5b3d56e7ecaa4efdca17cd7411ff904c1517855314/jiter-0.8.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e725edd0929fa79f8349ab4ec7f81c714df51dc4e991539a578e5018fa4a7152", size = 510822 }, + { url = "https://files.pythonhosted.org/packages/5c/37/3394bb47bac1ad2cb0465601f86828a0518d07828a650722e55268cdb7e6/jiter-0.8.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bf55846c7b7a680eebaf9c3c48d630e1bf51bdf76c68a5f654b8524335b0ad29", size = 503730 }, + { url = "https://files.pythonhosted.org/packages/f9/e2/253fc1fa59103bb4e3aa0665d6ceb1818df1cd7bf3eb492c4dad229b1cd4/jiter-0.8.2-cp312-cp312-win32.whl", hash = "sha256:7efe4853ecd3d6110301665a5178b9856be7e2a9485f49d91aa4d737ad2ae49e", size = 203375 }, + { url = "https://files.pythonhosted.org/packages/41/69/6d4bbe66b3b3b4507e47aa1dd5d075919ad242b4b1115b3f80eecd443687/jiter-0.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:83c0efd80b29695058d0fd2fa8a556490dbce9804eac3e281f373bbc99045f6c", size = 204740 }, +] + +[[package]] +name = "jsonschema" +version = "4.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/2e/03362ee4034a4c917f697890ccd4aec0800ccf9ded7f511971c75451deec/jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", size = 325778 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/4a/4f9dbeb84e8850557c02365a0eee0649abe5eb1d84af92a25731c6c0f922/jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566", size = 88462 }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2024.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/db/58f950c996c793472e336ff3655b13fbcf1e3b359dcf52dcf3ed3b52c352/jsonschema_specifications-2024.10.1.tar.gz", hash = "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272", size = 15561 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/0f/8910b19ac0670a0f80ce1008e5e751c4a57e14d2c4c13a482aa6079fa9d6/jsonschema_specifications-2024.10.1-py3-none-any.whl", hash = "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf", size = 18459 }, +] + +[[package]] +name = "lxml" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/f6/c15ca8e5646e937c148e147244817672cf920b56ac0bf2cc1512ae674be8/lxml-5.3.1.tar.gz", hash = "sha256:106b7b5d2977b339f1e97efe2778e2ab20e99994cbb0ec5e55771ed0795920c8", size = 3678591 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/4b/73426192004a643c11a644ed2346dbe72da164c8e775ea2e70f60e63e516/lxml-5.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a4058f16cee694577f7e4dd410263cd0ef75644b43802a689c2b3c2a7e69453b", size = 8142766 }, + { url = "https://files.pythonhosted.org/packages/30/c2/3b28f642b43fdf9580d936e8fdd3ec43c01a97ecfe17fd67f76ce9099752/lxml-5.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:364de8f57d6eda0c16dcfb999af902da31396949efa0e583e12675d09709881b", size = 4422744 }, + { url = "https://files.pythonhosted.org/packages/1f/a5/45279e464174b99d72d25bc018b097f9211c0925a174ca582a415609f036/lxml-5.3.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:528f3a0498a8edc69af0559bdcf8a9f5a8bf7c00051a6ef3141fdcf27017bbf5", size = 5229609 }, + { url = "https://files.pythonhosted.org/packages/f0/e7/10cd8b9e27ffb6b3465b76604725b67b7c70d4e399750ff88de1b38ab9eb/lxml-5.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db4743e30d6f5f92b6d2b7c86b3ad250e0bad8dee4b7ad8a0c44bfb276af89a3", size = 4943509 }, + { url = "https://files.pythonhosted.org/packages/ce/54/2d6f634924920b17122445136345d44c6d69178c9c49e161aa8f206739d6/lxml-5.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:17b5d7f8acf809465086d498d62a981fa6a56d2718135bb0e4aa48c502055f5c", size = 5561495 }, + { url = "https://files.pythonhosted.org/packages/a2/fe/7f5ae8fd1f357fcb21b0d4e20416fae870d654380b6487adbcaaf0df9b31/lxml-5.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:928e75a7200a4c09e6efc7482a1337919cc61fe1ba289f297827a5b76d8969c2", size = 4998970 }, + { url = "https://files.pythonhosted.org/packages/af/70/22fecb6f2ca8dc77d14ab6be3cef767ff8340040bc95dca384b5b1cb333a/lxml-5.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a997b784a639e05b9d4053ef3b20c7e447ea80814a762f25b8ed5a89d261eac", size = 5114205 }, + { url = "https://files.pythonhosted.org/packages/63/91/21619cc14f7fd1de3f1bdf86cc8106edacf4d685b540d658d84247a3a32a/lxml-5.3.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:7b82e67c5feb682dbb559c3e6b78355f234943053af61606af126df2183b9ef9", size = 4940823 }, + { url = "https://files.pythonhosted.org/packages/50/0f/27183248fa3cdd2040047ceccd320ff1ed1344167f38a4ac26aed092268b/lxml-5.3.1-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:f1de541a9893cf8a1b1db9bf0bf670a2decab42e3e82233d36a74eda7822b4c9", size = 5585725 }, + { url = "https://files.pythonhosted.org/packages/c6/8d/9b7388d5b23ed2f239a992a478cbd0ce313aaa2d008dd73c4042b190b6a9/lxml-5.3.1-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:de1fc314c3ad6bc2f6bd5b5a5b9357b8c6896333d27fdbb7049aea8bd5af2d79", size = 5082641 }, + { url = "https://files.pythonhosted.org/packages/65/8e/590e20833220eac55b6abcde71d3ae629d38ac1c3543bcc2bfe1f3c2f5d1/lxml-5.3.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:7c0536bd9178f754b277a3e53f90f9c9454a3bd108b1531ffff720e082d824f2", size = 5161219 }, + { url = "https://files.pythonhosted.org/packages/4e/77/cabdf5569fd0415a88ebd1d62d7f2814e71422439b8564aaa03e7eefc069/lxml-5.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:68018c4c67d7e89951a91fbd371e2e34cd8cfc71f0bb43b5332db38497025d51", size = 5019293 }, + { url = "https://files.pythonhosted.org/packages/49/bd/f0b6d50ea7b8b54aaa5df4410cb1d5ae6ffa016b8e0503cae08b86c24674/lxml-5.3.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aa826340a609d0c954ba52fd831f0fba2a4165659ab0ee1a15e4aac21f302406", size = 5651232 }, + { url = "https://files.pythonhosted.org/packages/fa/69/1793d00a4e3da7f27349edb5a6f3da947ed921263cd9a243fab11c6cbc07/lxml-5.3.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:796520afa499732191e39fc95b56a3b07f95256f2d22b1c26e217fb69a9db5b5", size = 5489527 }, + { url = "https://files.pythonhosted.org/packages/d3/c9/e2449129b6cb2054c898df8d850ea4dadd75b4c33695a6c4b0f35082f1e7/lxml-5.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3effe081b3135237da6e4c4530ff2a868d3f80be0bda027e118a5971285d42d0", size = 5227050 }, + { url = "https://files.pythonhosted.org/packages/ed/63/e5da540eba6ab9a0d4188eeaa5c85767b77cafa8efeb70da0593d6cd3b81/lxml-5.3.1-cp310-cp310-win32.whl", hash = "sha256:a22f66270bd6d0804b02cd49dae2b33d4341015545d17f8426f2c4e22f557a23", size = 3475345 }, + { url = "https://files.pythonhosted.org/packages/08/71/853a3ad812cd24c35b7776977cb0ae40c2b64ff79ad6d6c36c987daffc49/lxml-5.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:0bcfadea3cdc68e678d2b20cb16a16716887dd00a881e16f7d806c2138b8ff0c", size = 3805093 }, + { url = "https://files.pythonhosted.org/packages/57/bb/2faea15df82114fa27f2a86eec220506c532ee8ce211dff22f48881b353a/lxml-5.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e220f7b3e8656ab063d2eb0cd536fafef396829cafe04cb314e734f87649058f", size = 8161781 }, + { url = "https://files.pythonhosted.org/packages/9f/d3/374114084abb1f96026eccb6cd48b070f85de82fdabae6c2f1e198fa64e5/lxml-5.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f2cfae0688fd01f7056a17367e3b84f37c545fb447d7282cf2c242b16262607", size = 4432571 }, + { url = "https://files.pythonhosted.org/packages/0f/fb/44a46efdc235c2dd763c1e929611d8ff3b920c32b8fcd9051d38f4d04633/lxml-5.3.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:67d2f8ad9dcc3a9e826bdc7802ed541a44e124c29b7d95a679eeb58c1c14ade8", size = 5028919 }, + { url = "https://files.pythonhosted.org/packages/3b/e5/168ddf9f16a90b590df509858ae97a8219d6999d5a132ad9f72427454bed/lxml-5.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db0c742aad702fd5d0c6611a73f9602f20aec2007c102630c06d7633d9c8f09a", size = 4769599 }, + { url = "https://files.pythonhosted.org/packages/f9/0e/3e2742c6f4854b202eb8587c1f7ed760179f6a9fcb34a460497c8c8f3078/lxml-5.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:198bb4b4dd888e8390afa4f170d4fa28467a7eaf857f1952589f16cfbb67af27", size = 5369260 }, + { url = "https://files.pythonhosted.org/packages/b8/03/b2f2ab9e33c47609c80665e75efed258b030717e06693835413b34e797cb/lxml-5.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2a3e412ce1849be34b45922bfef03df32d1410a06d1cdeb793a343c2f1fd666", size = 4842798 }, + { url = "https://files.pythonhosted.org/packages/93/ad/0ecfb082b842358c8a9e3115ec944b7240f89821baa8cd7c0cb8a38e05cb/lxml-5.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b8969dbc8d09d9cd2ae06362c3bad27d03f433252601ef658a49bd9f2b22d79", size = 4917531 }, + { url = "https://files.pythonhosted.org/packages/64/5b/3e93d8ebd2b7eb984c2ad74dfff75493ce96e7b954b12e4f5fc34a700414/lxml-5.3.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5be8f5e4044146a69c96077c7e08f0709c13a314aa5315981185c1f00235fe65", size = 4791500 }, + { url = "https://files.pythonhosted.org/packages/91/83/7dc412362ee7a0259c7f64349393262525061fad551a1340ef92c59d9732/lxml-5.3.1-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:133f3493253a00db2c870d3740bc458ebb7d937bd0a6a4f9328373e0db305709", size = 5404557 }, + { url = "https://files.pythonhosted.org/packages/1e/41/c337f121d9dca148431f246825e021fa1a3f66a6b975deab1950530fdb04/lxml-5.3.1-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:52d82b0d436edd6a1d22d94a344b9a58abd6c68c357ed44f22d4ba8179b37629", size = 4931386 }, + { url = "https://files.pythonhosted.org/packages/a5/73/762c319c4906b3db67e4abc7cfe7d66c34996edb6d0e8cb60f462954d662/lxml-5.3.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b6f92e35e2658a5ed51c6634ceb5ddae32053182851d8cad2a5bc102a359b33", size = 4982124 }, + { url = "https://files.pythonhosted.org/packages/c1/e7/d1e296cb3b3b46371220a31350730948d7bea41cc9123c5fd219dea33c29/lxml-5.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:203b1d3eaebd34277be06a3eb880050f18a4e4d60861efba4fb946e31071a295", size = 4852742 }, + { url = "https://files.pythonhosted.org/packages/df/90/4adc854475105b93ead6c0c736f762d29371751340dcf5588cfcf8191b8a/lxml-5.3.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:155e1a5693cf4b55af652f5c0f78ef36596c7f680ff3ec6eb4d7d85367259b2c", size = 5457004 }, + { url = "https://files.pythonhosted.org/packages/f0/0d/39864efbd231c13eb53edee2ab91c742c24d2f93efe2af7d3fe4343e42c1/lxml-5.3.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22ec2b3c191f43ed21f9545e9df94c37c6b49a5af0a874008ddc9132d49a2d9c", size = 5298185 }, + { url = "https://files.pythonhosted.org/packages/8d/7a/630a64ceb1088196de182e2e33b5899691c3e1ae21af688e394208bd6810/lxml-5.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7eda194dd46e40ec745bf76795a7cccb02a6a41f445ad49d3cf66518b0bd9cff", size = 5032707 }, + { url = "https://files.pythonhosted.org/packages/b2/3d/091bc7b592333754cb346c1507ca948ab39bc89d83577ac8f1da3be4dece/lxml-5.3.1-cp311-cp311-win32.whl", hash = "sha256:fb7c61d4be18e930f75948705e9718618862e6fc2ed0d7159b2262be73f167a2", size = 3474288 }, + { url = "https://files.pythonhosted.org/packages/12/8c/7d47cfc0d04fd4e3639ec7e1c96c2561d5e890eb900de8f76eea75e0964a/lxml-5.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:c809eef167bf4a57af4b03007004896f5c60bd38dc3852fcd97a26eae3d4c9e6", size = 3815031 }, + { url = "https://files.pythonhosted.org/packages/3b/f4/5121aa9ee8e09b8b8a28cf3709552efe3d206ca51a20d6fa471b60bb3447/lxml-5.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e69add9b6b7b08c60d7ff0152c7c9a6c45b4a71a919be5abde6f98f1ea16421c", size = 8191889 }, + { url = "https://files.pythonhosted.org/packages/0a/ca/8e9aa01edddc74878f4aea85aa9ab64372f46aa804d1c36dda861bf9eabf/lxml-5.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4e52e1b148867b01c05e21837586ee307a01e793b94072d7c7b91d2c2da02ffe", size = 4450685 }, + { url = "https://files.pythonhosted.org/packages/b2/b3/ea40a5c98619fbd7e9349df7007994506d396b97620ced34e4e5053d3734/lxml-5.3.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4b382e0e636ed54cd278791d93fe2c4f370772743f02bcbe431a160089025c9", size = 5051722 }, + { url = "https://files.pythonhosted.org/packages/3a/5e/375418be35f8a695cadfe7e7412f16520e62e24952ed93c64c9554755464/lxml-5.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e49dc23a10a1296b04ca9db200c44d3eb32c8d8ec532e8c1fd24792276522a", size = 4786661 }, + { url = "https://files.pythonhosted.org/packages/79/7c/d258eaaa9560f6664f9b426a5165103015bee6512d8931e17342278bad0a/lxml-5.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4399b4226c4785575fb20998dc571bc48125dc92c367ce2602d0d70e0c455eb0", size = 5311766 }, + { url = "https://files.pythonhosted.org/packages/03/bc/a041415be4135a1b3fdf017a5d873244cc16689456166fbdec4b27fba153/lxml-5.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5412500e0dc5481b1ee9cf6b38bb3b473f6e411eb62b83dc9b62699c3b7b79f7", size = 4836014 }, + { url = "https://files.pythonhosted.org/packages/32/88/047f24967d5e3fc97848ea2c207eeef0f16239cdc47368c8b95a8dc93a33/lxml-5.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c93ed3c998ea8472be98fb55aed65b5198740bfceaec07b2eba551e55b7b9ae", size = 4961064 }, + { url = "https://files.pythonhosted.org/packages/3d/b5/ecf5a20937ecd21af02c5374020f4e3a3538e10a32379a7553fca3d77094/lxml-5.3.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:63d57fc94eb0bbb4735e45517afc21ef262991d8758a8f2f05dd6e4174944519", size = 4778341 }, + { url = "https://files.pythonhosted.org/packages/a4/05/56c359e07275911ed5f35ab1d63c8cd3360d395fb91e43927a2ae90b0322/lxml-5.3.1-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:b450d7cabcd49aa7ab46a3c6aa3ac7e1593600a1a0605ba536ec0f1b99a04322", size = 5345450 }, + { url = "https://files.pythonhosted.org/packages/b7/f4/f95e3ae12e9f32fbcde00f9affa6b0df07f495117f62dbb796a9a31c84d6/lxml-5.3.1-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:4df0ec814b50275ad6a99bc82a38b59f90e10e47714ac9871e1b223895825468", size = 4908336 }, + { url = "https://files.pythonhosted.org/packages/c5/f8/309546aec092434166a6e11c7dcecb5c2d0a787c18c072d61e18da9eba57/lxml-5.3.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d184f85ad2bb1f261eac55cddfcf62a70dee89982c978e92b9a74a1bfef2e367", size = 4986049 }, + { url = "https://files.pythonhosted.org/packages/71/1c/b951817cb5058ca7c332d012dfe8bc59dabd0f0a8911ddd7b7ea8e41cfbd/lxml-5.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b725e70d15906d24615201e650d5b0388b08a5187a55f119f25874d0103f90dd", size = 4860351 }, + { url = "https://files.pythonhosted.org/packages/31/23/45feba8dae1d35fcca1e51b051f59dc4223cbd23e071a31e25f3f73938a8/lxml-5.3.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a31fa7536ec1fb7155a0cd3a4e3d956c835ad0a43e3610ca32384d01f079ea1c", size = 5421580 }, + { url = "https://files.pythonhosted.org/packages/61/69/be245d7b2dbef81c542af59c97fcd641fbf45accf2dc1c325bae7d0d014c/lxml-5.3.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3c3c8b55c7fc7b7e8877b9366568cc73d68b82da7fe33d8b98527b73857a225f", size = 5285778 }, + { url = "https://files.pythonhosted.org/packages/69/06/128af2ed04bac99b8f83becfb74c480f1aa18407b5c329fad457e08a1bf4/lxml-5.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d61ec60945d694df806a9aec88e8f29a27293c6e424f8ff91c80416e3c617645", size = 5054455 }, + { url = "https://files.pythonhosted.org/packages/8a/2d/f03a21cf6cc75cdd083563e509c7b6b159d761115c4142abb5481094ed8c/lxml-5.3.1-cp312-cp312-win32.whl", hash = "sha256:f4eac0584cdc3285ef2e74eee1513a6001681fd9753b259e8159421ed28a72e5", size = 3486315 }, + { url = "https://files.pythonhosted.org/packages/2b/9c/8abe21585d20ef70ad9cec7562da4332b764ed69ec29b7389d23dfabcea0/lxml-5.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:29bfc8d3d88e56ea0a27e7c4897b642706840247f59f4377d81be8f32aa0cfbf", size = 3816925 }, + { url = "https://files.pythonhosted.org/packages/d2/b4/89a68d05f267f05cc1b8b2f289a8242955705b1b0a9d246198227817ee46/lxml-5.3.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:afa578b6524ff85fb365f454cf61683771d0170470c48ad9d170c48075f86725", size = 3936118 }, + { url = "https://files.pythonhosted.org/packages/7f/0d/c034a541e7a1153527d7880c62493a74f2277f38e64de2480cadd0d4cf96/lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f5e80adf0aafc7b5454f2c1cb0cde920c9b1f2cbd0485f07cc1d0497c35c5d", size = 4233690 }, + { url = "https://files.pythonhosted.org/packages/35/5c/38e183c2802f14fbdaa75c3266e11d0ca05c64d78e8cdab2ee84e954a565/lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dd0b80ac2d8f13ffc906123a6f20b459cb50a99222d0da492360512f3e50f84", size = 4349569 }, + { url = "https://files.pythonhosted.org/packages/18/5b/14f93b359b3c29673d5d282bc3a6edb3a629879854a77541841aba37607f/lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:422c179022ecdedbe58b0e242607198580804253da220e9454ffe848daa1cfd2", size = 4236731 }, + { url = "https://files.pythonhosted.org/packages/f6/08/8471de65f3dee70a3a50e7082fd7409f0ac7a1ace777c13fca4aea1a5759/lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:524ccfded8989a6595dbdda80d779fb977dbc9a7bc458864fc9a0c2fc15dc877", size = 4373119 }, + { url = "https://files.pythonhosted.org/packages/83/29/00b9b0322a473aee6cda87473401c9abb19506cd650cc69a8aa38277ea74/lxml-5.3.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:48fd46bf7155def2e15287c6f2b133a2f78e2d22cdf55647269977b873c65499", size = 3487718 }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357 }, + { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393 }, + { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732 }, + { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866 }, + { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964 }, + { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977 }, + { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366 }, + { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091 }, + { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065 }, + { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514 }, + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198 }, +] + +[[package]] +name = "msgpack" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/d0/7555686ae7ff5731205df1012ede15dd9d927f6227ea151e901c7406af4f/msgpack-1.1.0.tar.gz", hash = "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e", size = 167260 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/f9/a892a6038c861fa849b11a2bb0502c07bc698ab6ea53359e5771397d883b/msgpack-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd", size = 150428 }, + { url = "https://files.pythonhosted.org/packages/df/7a/d174cc6a3b6bb85556e6a046d3193294a92f9a8e583cdbd46dc8a1d7e7f4/msgpack-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d", size = 84131 }, + { url = "https://files.pythonhosted.org/packages/08/52/bf4fbf72f897a23a56b822997a72c16de07d8d56d7bf273242f884055682/msgpack-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:914571a2a5b4e7606997e169f64ce53a8b1e06f2cf2c3a7273aa106236d43dd5", size = 81215 }, + { url = "https://files.pythonhosted.org/packages/02/95/dc0044b439b518236aaf012da4677c1b8183ce388411ad1b1e63c32d8979/msgpack-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c921af52214dcbb75e6bdf6a661b23c3e6417f00c603dd2070bccb5c3ef499f5", size = 371229 }, + { url = "https://files.pythonhosted.org/packages/ff/75/09081792db60470bef19d9c2be89f024d366b1e1973c197bb59e6aabc647/msgpack-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8ce0b22b890be5d252de90d0e0d119f363012027cf256185fc3d474c44b1b9e", size = 378034 }, + { url = "https://files.pythonhosted.org/packages/32/d3/c152e0c55fead87dd948d4b29879b0f14feeeec92ef1fd2ec21b107c3f49/msgpack-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:73322a6cc57fcee3c0c57c4463d828e9428275fb85a27aa2aa1a92fdc42afd7b", size = 363070 }, + { url = "https://files.pythonhosted.org/packages/d9/2c/82e73506dd55f9e43ac8aa007c9dd088c6f0de2aa19e8f7330e6a65879fc/msgpack-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e1f3c3d21f7cf67bcf2da8e494d30a75e4cf60041d98b3f79875afb5b96f3a3f", size = 359863 }, + { url = "https://files.pythonhosted.org/packages/cb/a0/3d093b248837094220e1edc9ec4337de3443b1cfeeb6e0896af8ccc4cc7a/msgpack-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64fc9068d701233effd61b19efb1485587560b66fe57b3e50d29c5d78e7fef68", size = 368166 }, + { url = "https://files.pythonhosted.org/packages/e4/13/7646f14f06838b406cf5a6ddbb7e8dc78b4996d891ab3b93c33d1ccc8678/msgpack-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:42f754515e0f683f9c79210a5d1cad631ec3d06cea5172214d2176a42e67e19b", size = 370105 }, + { url = "https://files.pythonhosted.org/packages/67/fa/dbbd2443e4578e165192dabbc6a22c0812cda2649261b1264ff515f19f15/msgpack-1.1.0-cp310-cp310-win32.whl", hash = "sha256:3df7e6b05571b3814361e8464f9304c42d2196808e0119f55d0d3e62cd5ea044", size = 68513 }, + { url = "https://files.pythonhosted.org/packages/24/ce/c2c8fbf0ded750cb63cbcbb61bc1f2dfd69e16dca30a8af8ba80ec182dcd/msgpack-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:685ec345eefc757a7c8af44a3032734a739f8c45d1b0ac45efc5d8977aa4720f", size = 74687 }, + { url = "https://files.pythonhosted.org/packages/b7/5e/a4c7154ba65d93be91f2f1e55f90e76c5f91ccadc7efc4341e6f04c8647f/msgpack-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d364a55082fb2a7416f6c63ae383fbd903adb5a6cf78c5b96cc6316dc1cedc7", size = 150803 }, + { url = "https://files.pythonhosted.org/packages/60/c2/687684164698f1d51c41778c838d854965dd284a4b9d3a44beba9265c931/msgpack-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79ec007767b9b56860e0372085f8504db5d06bd6a327a335449508bbee9648fa", size = 84343 }, + { url = "https://files.pythonhosted.org/packages/42/ae/d3adea9bb4a1342763556078b5765e666f8fdf242e00f3f6657380920972/msgpack-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ad622bf7756d5a497d5b6836e7fc3752e2dd6f4c648e24b1803f6048596f701", size = 81408 }, + { url = "https://files.pythonhosted.org/packages/dc/17/6313325a6ff40ce9c3207293aee3ba50104aed6c2c1559d20d09e5c1ff54/msgpack-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e59bca908d9ca0de3dc8684f21ebf9a690fe47b6be93236eb40b99af28b6ea6", size = 396096 }, + { url = "https://files.pythonhosted.org/packages/a8/a1/ad7b84b91ab5a324e707f4c9761633e357820b011a01e34ce658c1dda7cc/msgpack-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1da8f11a3dd397f0a32c76165cf0c4eb95b31013a94f6ecc0b280c05c91b59", size = 403671 }, + { url = "https://files.pythonhosted.org/packages/bb/0b/fd5b7c0b308bbf1831df0ca04ec76fe2f5bf6319833646b0a4bd5e9dc76d/msgpack-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:452aff037287acb1d70a804ffd022b21fa2bb7c46bee884dbc864cc9024128a0", size = 387414 }, + { url = "https://files.pythonhosted.org/packages/f0/03/ff8233b7c6e9929a1f5da3c7860eccd847e2523ca2de0d8ef4878d354cfa/msgpack-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8da4bf6d54ceed70e8861f833f83ce0814a2b72102e890cbdfe4b34764cdd66e", size = 383759 }, + { url = "https://files.pythonhosted.org/packages/1f/1b/eb82e1fed5a16dddd9bc75f0854b6e2fe86c0259c4353666d7fab37d39f4/msgpack-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:41c991beebf175faf352fb940bf2af9ad1fb77fd25f38d9142053914947cdbf6", size = 394405 }, + { url = "https://files.pythonhosted.org/packages/90/2e/962c6004e373d54ecf33d695fb1402f99b51832631e37c49273cc564ffc5/msgpack-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a52a1f3a5af7ba1c9ace055b659189f6c669cf3657095b50f9602af3a3ba0fe5", size = 396041 }, + { url = "https://files.pythonhosted.org/packages/f8/20/6e03342f629474414860c48aeffcc2f7f50ddaf351d95f20c3f1c67399a8/msgpack-1.1.0-cp311-cp311-win32.whl", hash = "sha256:58638690ebd0a06427c5fe1a227bb6b8b9fdc2bd07701bec13c2335c82131a88", size = 68538 }, + { url = "https://files.pythonhosted.org/packages/aa/c4/5a582fc9a87991a3e6f6800e9bb2f3c82972912235eb9539954f3e9997c7/msgpack-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd2906780f25c8ed5d7b323379f6138524ba793428db5d0e9d226d3fa6aa1788", size = 74871 }, + { url = "https://files.pythonhosted.org/packages/e1/d6/716b7ca1dbde63290d2973d22bbef1b5032ca634c3ff4384a958ec3f093a/msgpack-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d46cf9e3705ea9485687aa4001a76e44748b609d260af21c4ceea7f2212a501d", size = 152421 }, + { url = "https://files.pythonhosted.org/packages/70/da/5312b067f6773429cec2f8f08b021c06af416bba340c912c2ec778539ed6/msgpack-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5dbad74103df937e1325cc4bfeaf57713be0b4f15e1c2da43ccdd836393e2ea2", size = 85277 }, + { url = "https://files.pythonhosted.org/packages/28/51/da7f3ae4462e8bb98af0d5bdf2707f1b8c65a0d4f496e46b6afb06cbc286/msgpack-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58dfc47f8b102da61e8949708b3eafc3504509a5728f8b4ddef84bd9e16ad420", size = 82222 }, + { url = "https://files.pythonhosted.org/packages/33/af/dc95c4b2a49cff17ce47611ca9ba218198806cad7796c0b01d1e332c86bb/msgpack-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676e5be1b472909b2ee6356ff425ebedf5142427842aa06b4dfd5117d1ca8a2", size = 392971 }, + { url = "https://files.pythonhosted.org/packages/f1/54/65af8de681fa8255402c80eda2a501ba467921d5a7a028c9c22a2c2eedb5/msgpack-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17fb65dd0bec285907f68b15734a993ad3fc94332b5bb21b0435846228de1f39", size = 401403 }, + { url = "https://files.pythonhosted.org/packages/97/8c/e333690777bd33919ab7024269dc3c41c76ef5137b211d776fbb404bfead/msgpack-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a51abd48c6d8ac89e0cfd4fe177c61481aca2d5e7ba42044fd218cfd8ea9899f", size = 385356 }, + { url = "https://files.pythonhosted.org/packages/57/52/406795ba478dc1c890559dd4e89280fa86506608a28ccf3a72fbf45df9f5/msgpack-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2137773500afa5494a61b1208619e3871f75f27b03bcfca7b3a7023284140247", size = 383028 }, + { url = "https://files.pythonhosted.org/packages/e7/69/053b6549bf90a3acadcd8232eae03e2fefc87f066a5b9fbb37e2e608859f/msgpack-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:398b713459fea610861c8a7b62a6fec1882759f308ae0795b5413ff6a160cf3c", size = 391100 }, + { url = "https://files.pythonhosted.org/packages/23/f0/d4101d4da054f04274995ddc4086c2715d9b93111eb9ed49686c0f7ccc8a/msgpack-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06f5fd2f6bb2a7914922d935d3b8bb4a7fff3a9a91cfce6d06c13bc42bec975b", size = 394254 }, + { url = "https://files.pythonhosted.org/packages/1c/12/cf07458f35d0d775ff3a2dc5559fa2e1fcd06c46f1ef510e594ebefdca01/msgpack-1.1.0-cp312-cp312-win32.whl", hash = "sha256:ad33e8400e4ec17ba782f7b9cf868977d867ed784a1f5f2ab46e7ba53b6e1e1b", size = 69085 }, + { url = "https://files.pythonhosted.org/packages/73/80/2708a4641f7d553a63bc934a3eb7214806b5b39d200133ca7f7afb0a53e8/msgpack-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:115a7af8ee9e8cddc10f87636767857e7e3717b7a2e97379dc2054712693e90f", size = 75347 }, +] + +[[package]] +name = "mypy" +version = "1.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/f8/65a7ce8d0e09b6329ad0c8d40330d100ea343bd4dd04c4f8ae26462d0a17/mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13", size = 10738433 }, + { url = "https://files.pythonhosted.org/packages/b4/95/9c0ecb8eacfe048583706249439ff52105b3f552ea9c4024166c03224270/mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559", size = 9861472 }, + { url = "https://files.pythonhosted.org/packages/84/09/9ec95e982e282e20c0d5407bc65031dfd0f0f8ecc66b69538296e06fcbee/mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b", size = 11611424 }, + { url = "https://files.pythonhosted.org/packages/78/13/f7d14e55865036a1e6a0a69580c240f43bc1f37407fe9235c0d4ef25ffb0/mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3", size = 12365450 }, + { url = "https://files.pythonhosted.org/packages/48/e1/301a73852d40c241e915ac6d7bcd7fedd47d519246db2d7b86b9d7e7a0cb/mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b", size = 12551765 }, + { url = "https://files.pythonhosted.org/packages/77/ba/c37bc323ae5fe7f3f15a28e06ab012cd0b7552886118943e90b15af31195/mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828", size = 9274701 }, + { url = "https://files.pythonhosted.org/packages/03/bc/f6339726c627bd7ca1ce0fa56c9ae2d0144604a319e0e339bdadafbbb599/mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f", size = 10662338 }, + { url = "https://files.pythonhosted.org/packages/e2/90/8dcf506ca1a09b0d17555cc00cd69aee402c203911410136cd716559efe7/mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5", size = 9787540 }, + { url = "https://files.pythonhosted.org/packages/05/05/a10f9479681e5da09ef2f9426f650d7b550d4bafbef683b69aad1ba87457/mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e", size = 11538051 }, + { url = "https://files.pythonhosted.org/packages/e9/9a/1f7d18b30edd57441a6411fcbc0c6869448d1a4bacbaee60656ac0fc29c8/mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c", size = 12286751 }, + { url = "https://files.pythonhosted.org/packages/72/af/19ff499b6f1dafcaf56f9881f7a965ac2f474f69f6f618b5175b044299f5/mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f", size = 12421783 }, + { url = "https://files.pythonhosted.org/packages/96/39/11b57431a1f686c1aed54bf794870efe0f6aeca11aca281a0bd87a5ad42c/mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f", size = 9265618 }, + { url = "https://files.pythonhosted.org/packages/98/3a/03c74331c5eb8bd025734e04c9840532226775c47a2c39b56a0c8d4f128d/mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd", size = 10793981 }, + { url = "https://files.pythonhosted.org/packages/f0/1a/41759b18f2cfd568848a37c89030aeb03534411eef981df621d8fad08a1d/mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f", size = 9749175 }, + { url = "https://files.pythonhosted.org/packages/12/7e/873481abf1ef112c582db832740f4c11b2bfa510e829d6da29b0ab8c3f9c/mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464", size = 11455675 }, + { url = "https://files.pythonhosted.org/packages/b3/d0/92ae4cde706923a2d3f2d6c39629134063ff64b9dedca9c1388363da072d/mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee", size = 12410020 }, + { url = "https://files.pythonhosted.org/packages/46/8b/df49974b337cce35f828ba6fda228152d6db45fed4c86ba56ffe442434fd/mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e", size = 12498582 }, + { url = "https://files.pythonhosted.org/packages/13/50/da5203fcf6c53044a0b699939f31075c45ae8a4cadf538a9069b165c1050/mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22", size = 9366614 }, + { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777 }, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, +] + +[[package]] +name = "narwhals" +version = "1.27.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/d6/1dadff863b95e4ec74eaba7979278e446699532136c74183a398778b1949/narwhals-1.27.1.tar.gz", hash = "sha256:68505d0cee1e6c00382ac8b65e922f8b694a11cbe482a057fa63139de8d0ea03", size = 251670 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/ea/dc14822a0a75e027562f081eb638417b1b7845e1e01dd85c5b6573ebf1b2/narwhals-1.27.1-py3-none-any.whl", hash = "sha256:71e4a126007886e3dd9d71d0d5921ebd2e8c1f9be9c405fe11850ece2b066c59", size = 308837 }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, +] + +[[package]] +name = "numpy" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/90/8956572f5c4ae52201fdec7ba2044b2c882832dcec7d5d0922c9e9acf2de/numpy-2.2.3.tar.gz", hash = "sha256:dbdc15f0c81611925f382dfa97b3bd0bc2c1ce19d4fe50482cb0ddc12ba30020", size = 20262700 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/e1/1816d5d527fa870b260a1c2c5904d060caad7515637bd54f495a5ce13ccd/numpy-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cbc6472e01952d3d1b2772b720428f8b90e2deea8344e854df22b0618e9cce71", size = 21232911 }, + { url = "https://files.pythonhosted.org/packages/29/46/9f25dc19b359f10c0e52b6bac25d3181eb1f4b4d04c9846a32cf5ea52762/numpy-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdfe0c22692a30cd830c0755746473ae66c4a8f2e7bd508b35fb3b6a0813d787", size = 14371955 }, + { url = "https://files.pythonhosted.org/packages/72/d7/de941296e6b09a5c81d3664ad912f1496a0ecdd2f403318e5e35604ff70f/numpy-2.2.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:e37242f5324ffd9f7ba5acf96d774f9276aa62a966c0bad8dae692deebec7716", size = 5410476 }, + { url = "https://files.pythonhosted.org/packages/36/ce/55f685995110f8a268fdca0f198c9a84fa87b39512830965cc1087af6391/numpy-2.2.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:95172a21038c9b423e68be78fd0be6e1b97674cde269b76fe269a5dfa6fadf0b", size = 6945730 }, + { url = "https://files.pythonhosted.org/packages/4f/84/abdb9f6e22576d89c259401c3234d4755b322539491bbcffadc8bcb120d3/numpy-2.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5b47c440210c5d1d67e1cf434124e0b5c395eee1f5806fdd89b553ed1acd0a3", size = 14350752 }, + { url = "https://files.pythonhosted.org/packages/e9/88/3870cfa9bef4dffb3a326507f430e6007eeac258ebeef6b76fc542aef66d/numpy-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0391ea3622f5c51a2e29708877d56e3d276827ac5447d7f45e9bc4ade8923c52", size = 16399386 }, + { url = "https://files.pythonhosted.org/packages/02/10/3f629682dd0b457525c131945329c4e81e2dadeb11256e6ce4c9a1a6fb41/numpy-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f6b3dfc7661f8842babd8ea07e9897fe3d9b69a1d7e5fbb743e4160f9387833b", size = 15561826 }, + { url = "https://files.pythonhosted.org/packages/da/18/fd35673ba9751eba449d4ce5d24d94e3b612cdbfba79348da71488c0b7ac/numpy-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1ad78ce7f18ce4e7df1b2ea4019b5817a2f6a8a16e34ff2775f646adce0a5027", size = 18188593 }, + { url = "https://files.pythonhosted.org/packages/ce/4c/c0f897b580ea59484b4cc96a441fea50333b26675a60a1421bc912268b5f/numpy-2.2.3-cp310-cp310-win32.whl", hash = "sha256:5ebeb7ef54a7be11044c33a17b2624abe4307a75893c001a4800857956b41094", size = 6590421 }, + { url = "https://files.pythonhosted.org/packages/e5/5b/aaabbfc7060c5c8f0124c5deb5e114a3b413a548bbc64e372c5b5db36165/numpy-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:596140185c7fa113563c67c2e894eabe0daea18cf8e33851738c19f70ce86aeb", size = 12925667 }, + { url = "https://files.pythonhosted.org/packages/96/86/453aa3949eab6ff54e2405f9cb0c01f756f031c3dc2a6d60a1d40cba5488/numpy-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:16372619ee728ed67a2a606a614f56d3eabc5b86f8b615c79d01957062826ca8", size = 21237256 }, + { url = "https://files.pythonhosted.org/packages/20/c3/93ecceadf3e155d6a9e4464dd2392d8d80cf436084c714dc8535121c83e8/numpy-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5521a06a3148686d9269c53b09f7d399a5725c47bbb5b35747e1cb76326b714b", size = 14408049 }, + { url = "https://files.pythonhosted.org/packages/8d/29/076999b69bd9264b8df5e56f2be18da2de6b2a2d0e10737e5307592e01de/numpy-2.2.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:7c8dde0ca2f77828815fd1aedfdf52e59071a5bae30dac3b4da2a335c672149a", size = 5408655 }, + { url = "https://files.pythonhosted.org/packages/e2/a7/b14f0a73eb0fe77cb9bd5b44534c183b23d4229c099e339c522724b02678/numpy-2.2.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:77974aba6c1bc26e3c205c2214f0d5b4305bdc719268b93e768ddb17e3fdd636", size = 6949996 }, + { url = "https://files.pythonhosted.org/packages/72/2f/8063da0616bb0f414b66dccead503bd96e33e43685c820e78a61a214c098/numpy-2.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d42f9c36d06440e34226e8bd65ff065ca0963aeecada587b937011efa02cdc9d", size = 14355789 }, + { url = "https://files.pythonhosted.org/packages/e6/d7/3cd47b00b8ea95ab358c376cf5602ad21871410950bc754cf3284771f8b6/numpy-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2712c5179f40af9ddc8f6727f2bd910ea0eb50206daea75f58ddd9fa3f715bb", size = 16411356 }, + { url = "https://files.pythonhosted.org/packages/27/c0/a2379e202acbb70b85b41483a422c1e697ff7eee74db642ca478de4ba89f/numpy-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c8b0451d2ec95010d1db8ca733afc41f659f425b7f608af569711097fd6014e2", size = 15576770 }, + { url = "https://files.pythonhosted.org/packages/bc/63/a13ee650f27b7999e5b9e1964ae942af50bb25606d088df4229283eda779/numpy-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9b4a8148c57ecac25a16b0e11798cbe88edf5237b0df99973687dd866f05e1b", size = 18200483 }, + { url = "https://files.pythonhosted.org/packages/4c/87/e71f89935e09e8161ac9c590c82f66d2321eb163893a94af749dfa8a3cf8/numpy-2.2.3-cp311-cp311-win32.whl", hash = "sha256:1f45315b2dc58d8a3e7754fe4e38b6fce132dab284a92851e41b2b344f6441c5", size = 6588415 }, + { url = "https://files.pythonhosted.org/packages/b9/c6/cd4298729826af9979c5f9ab02fcaa344b82621e7c49322cd2d210483d3f/numpy-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f48ba6f6c13e5e49f3d3efb1b51c8193215c42ac82610a04624906a9270be6f", size = 12929604 }, + { url = "https://files.pythonhosted.org/packages/43/ec/43628dcf98466e087812142eec6d1c1a6c6bdfdad30a0aa07b872dc01f6f/numpy-2.2.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12c045f43b1d2915eca6b880a7f4a256f59d62df4f044788c8ba67709412128d", size = 20929458 }, + { url = "https://files.pythonhosted.org/packages/9b/c0/2f4225073e99a5c12350954949ed19b5d4a738f541d33e6f7439e33e98e4/numpy-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:87eed225fd415bbae787f93a457af7f5990b92a334e346f72070bf569b9c9c95", size = 14115299 }, + { url = "https://files.pythonhosted.org/packages/ca/fa/d2c5575d9c734a7376cc1592fae50257ec95d061b27ee3dbdb0b3b551eb2/numpy-2.2.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:712a64103d97c404e87d4d7c47fb0c7ff9acccc625ca2002848e0d53288b90ea", size = 5145723 }, + { url = "https://files.pythonhosted.org/packages/eb/dc/023dad5b268a7895e58e791f28dc1c60eb7b6c06fcbc2af8538ad069d5f3/numpy-2.2.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a5ae282abe60a2db0fd407072aff4599c279bcd6e9a2475500fc35b00a57c532", size = 6678797 }, + { url = "https://files.pythonhosted.org/packages/3f/19/bcd641ccf19ac25abb6fb1dcd7744840c11f9d62519d7057b6ab2096eb60/numpy-2.2.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5266de33d4c3420973cf9ae3b98b54a2a6d53a559310e3236c4b2b06b9c07d4e", size = 14067362 }, + { url = "https://files.pythonhosted.org/packages/39/04/78d2e7402fb479d893953fb78fa7045f7deb635ec095b6b4f0260223091a/numpy-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b787adbf04b0db1967798dba8da1af07e387908ed1553a0d6e74c084d1ceafe", size = 16116679 }, + { url = "https://files.pythonhosted.org/packages/d0/a1/e90f7aa66512be3150cb9d27f3d9995db330ad1b2046474a13b7040dfd92/numpy-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:34c1b7e83f94f3b564b35f480f5652a47007dd91f7c839f404d03279cc8dd021", size = 15264272 }, + { url = "https://files.pythonhosted.org/packages/dc/b6/50bd027cca494de4fa1fc7bf1662983d0ba5f256fa0ece2c376b5eb9b3f0/numpy-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4d8335b5f1b6e2bce120d55fb17064b0262ff29b459e8493d1785c18ae2553b8", size = 17880549 }, + { url = "https://files.pythonhosted.org/packages/96/30/f7bf4acb5f8db10a96f73896bdeed7a63373137b131ca18bd3dab889db3b/numpy-2.2.3-cp312-cp312-win32.whl", hash = "sha256:4d9828d25fb246bedd31e04c9e75714a4087211ac348cb39c8c5f99dbb6683fe", size = 6293394 }, + { url = "https://files.pythonhosted.org/packages/42/6e/55580a538116d16ae7c9aa17d4edd56e83f42126cb1dfe7a684da7925d2c/numpy-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:83807d445817326b4bcdaaaf8e8e9f1753da04341eceec705c001ff342002e5d", size = 12626357 }, + { url = "https://files.pythonhosted.org/packages/0a/b5/a7839f5478be8f859cb880f13d90fcfe4b0ec7a9ebaff2bcc30d96760596/numpy-2.2.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3c2ec8a0f51d60f1e9c0c5ab116b7fc104b165ada3f6c58abf881cb2eb16044d", size = 21064244 }, + { url = "https://files.pythonhosted.org/packages/29/e8/5da32ffcaa7a72f7ecd82f90c062140a061eb823cb88e90279424e515cf4/numpy-2.2.3-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:ed2cf9ed4e8ebc3b754d398cba12f24359f018b416c380f577bbae112ca52fc9", size = 6809418 }, + { url = "https://files.pythonhosted.org/packages/a8/a9/68aa7076c7656a7308a0f73d0a2ced8c03f282c9fd98fa7ce21c12634087/numpy-2.2.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39261798d208c3095ae4f7bc8eaeb3481ea8c6e03dc48028057d3cbdbdb8937e", size = 16215461 }, + { url = "https://files.pythonhosted.org/packages/17/7f/d322a4125405920401450118dbdc52e0384026bd669939484670ce8b2ab9/numpy-2.2.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:783145835458e60fa97afac25d511d00a1eca94d4a8f3ace9fe2043003c678e4", size = 12839607 }, +] + +[[package]] +name = "onnx" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/54/0e385c26bf230d223810a9c7d06628d954008a5e5e4b73ee26ef02327282/onnx-1.17.0.tar.gz", hash = "sha256:48ca1a91ff73c1d5e3ea2eef20ae5d0e709bb8a2355ed798ffc2169753013fd3", size = 12165120 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/29/57053ba7787788ac75efb095cfc1ae290436b6d3a26754693cd7ed1b4fac/onnx-1.17.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:38b5df0eb22012198cdcee527cc5f917f09cce1f88a69248aaca22bd78a7f023", size = 16645616 }, + { url = "https://files.pythonhosted.org/packages/75/0d/831807a18db2a5e8f7813848c59272b904a4ef3939fe4d1288cbce9ea735/onnx-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d545335cb49d4d8c47cc803d3a805deb7ad5d9094dc67657d66e568610a36d7d", size = 15908420 }, + { url = "https://files.pythonhosted.org/packages/dd/5b/c4f95dbe652d14aeba9afaceb177e9ffc48ac3c03048dd3f872f26f07e34/onnx-1.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3193a3672fc60f1a18c0f4c93ac81b761bc72fd8a6c2035fa79ff5969f07713e", size = 16046244 }, + { url = "https://files.pythonhosted.org/packages/08/a9/c1f218085043dccc6311460239e253fa6957cf12ee4b0a56b82014938d0b/onnx-1.17.0-cp310-cp310-win32.whl", hash = "sha256:0141c2ce806c474b667b7e4499164227ef594584da432fd5613ec17c1855e311", size = 14423516 }, + { url = "https://files.pythonhosted.org/packages/0e/d3/d26ebf590a65686dde6b27fef32493026c5be9e42083340d947395f93405/onnx-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:dfd777d95c158437fda6b34758f0877d15b89cbe9ff45affbedc519b35345cf9", size = 14528496 }, + { url = "https://files.pythonhosted.org/packages/e5/a9/8d1b1d53aec70df53e0f57e9f9fcf47004276539e29230c3d5f1f50719ba/onnx-1.17.0-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:d6fc3a03fc0129b8b6ac03f03bc894431ffd77c7d79ec023d0afd667b4d35869", size = 16647991 }, + { url = "https://files.pythonhosted.org/packages/7b/e3/cc80110e5996ca61878f7b4c73c7a286cd88918ff35eacb60dc75ab11ef5/onnx-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f01a4b63d4e1d8ec3e2f069e7b798b2955810aa434f7361f01bc8ca08d69cce4", size = 15908949 }, + { url = "https://files.pythonhosted.org/packages/b1/2f/91092557ed478e323a2b4471e2081fdf88d1dd52ae988ceaf7db4e4506ff/onnx-1.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a183c6178be001bf398260e5ac2c927dc43e7746e8638d6c05c20e321f8c949", size = 16048190 }, + { url = "https://files.pythonhosted.org/packages/ac/59/9ea23fc22d0bb853133f363e6248e31bcbc6c1c90543a3938c00412ac02a/onnx-1.17.0-cp311-cp311-win32.whl", hash = "sha256:081ec43a8b950171767d99075b6b92553901fa429d4bc5eb3ad66b36ef5dbe3a", size = 14424299 }, + { url = "https://files.pythonhosted.org/packages/51/a5/19b0dfcb567b62e7adf1a21b08b23224f0c2d13842aee4d0abc6f07f9cf5/onnx-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:95c03e38671785036bb704c30cd2e150825f6ab4763df3a4f1d249da48525957", size = 14529142 }, + { url = "https://files.pythonhosted.org/packages/b4/dd/c416a11a28847fafb0db1bf43381979a0f522eb9107b831058fde012dd56/onnx-1.17.0-cp312-cp312-macosx_12_0_universal2.whl", hash = "sha256:0e906e6a83437de05f8139ea7eaf366bf287f44ae5cc44b2850a30e296421f2f", size = 16651271 }, + { url = "https://files.pythonhosted.org/packages/f0/6c/f040652277f514ecd81b7251841f96caa5538365af7df07f86c6018cda2b/onnx-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d955ba2939878a520a97614bcf2e79c1df71b29203e8ced478fa78c9a9c63c2", size = 15907522 }, + { url = "https://files.pythonhosted.org/packages/3d/7c/67f4952d1b56b3f74a154b97d0dd0630d525923b354db117d04823b8b49b/onnx-1.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f3fb5cc4e2898ac5312a7dc03a65133dd2abf9a5e520e69afb880a7251ec97a", size = 16046307 }, + { url = "https://files.pythonhosted.org/packages/ae/20/6da11042d2ab870dfb4ce4a6b52354d7651b6b4112038b6d2229ab9904c4/onnx-1.17.0-cp312-cp312-win32.whl", hash = "sha256:317870fca3349d19325a4b7d1b5628f6de3811e9710b1e3665c68b073d0e68d7", size = 14424235 }, + { url = "https://files.pythonhosted.org/packages/35/55/c4d11bee1fdb0c4bd84b4e3562ff811a19b63266816870ae1f95567aa6e1/onnx-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:659b8232d627a5460d74fd3c96947ae83db6d03f035ac633e20cd69cfa029227", size = 14530453 }, +] + +[[package]] +name = "onnxruntime" +version = "1.20.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coloredlogs" }, + { name = "flatbuffers" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "protobuf" }, + { name = "sympy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/28/99f903b0eb1cd6f3faa0e343217d9fb9f47b84bca98bd9859884631336ee/onnxruntime-1.20.1-cp310-cp310-macosx_13_0_universal2.whl", hash = "sha256:e50ba5ff7fed4f7d9253a6baf801ca2883cc08491f9d32d78a80da57256a5439", size = 30996314 }, + { url = "https://files.pythonhosted.org/packages/6d/c6/c4c0860bee2fde6037bdd9dcd12d323f6e38cf00fcc9a5065b394337fc55/onnxruntime-1.20.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b2908b50101a19e99c4d4e97ebb9905561daf61829403061c1adc1b588bc0de", size = 11954010 }, + { url = "https://files.pythonhosted.org/packages/63/47/3dc0b075ab539f16b3d8b09df6b504f51836086ee709690a6278d791737d/onnxruntime-1.20.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d82daaec24045a2e87598b8ac2b417b1cce623244e80e663882e9fe1aae86410", size = 13330452 }, + { url = "https://files.pythonhosted.org/packages/27/ef/80fab86289ecc01a734b7ddf115dfb93d8b2e004bd1e1977e12881c72b12/onnxruntime-1.20.1-cp310-cp310-win32.whl", hash = "sha256:4c4b251a725a3b8cf2aab284f7d940c26094ecd9d442f07dd81ab5470e99b83f", size = 9813849 }, + { url = "https://files.pythonhosted.org/packages/a9/e6/33ab10066c9875a29d55e66ae97c3bf91b9b9b987179455d67c32261a49c/onnxruntime-1.20.1-cp310-cp310-win_amd64.whl", hash = "sha256:d3b616bb53a77a9463707bb313637223380fc327f5064c9a782e8ec69c22e6a2", size = 11329702 }, + { url = "https://files.pythonhosted.org/packages/95/8d/2634e2959b34aa8a0037989f4229e9abcfa484e9c228f99633b3241768a6/onnxruntime-1.20.1-cp311-cp311-macosx_13_0_universal2.whl", hash = "sha256:06bfbf02ca9ab5f28946e0f912a562a5f005301d0c419283dc57b3ed7969bb7b", size = 30998725 }, + { url = "https://files.pythonhosted.org/packages/a5/da/c44bf9bd66cd6d9018a921f053f28d819445c4d84b4dd4777271b0fe52a2/onnxruntime-1.20.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6243e34d74423bdd1edf0ae9596dd61023b260f546ee17d701723915f06a9f7", size = 11955227 }, + { url = "https://files.pythonhosted.org/packages/11/ac/4120dfb74c8e45cce1c664fc7f7ce010edd587ba67ac41489f7432eb9381/onnxruntime-1.20.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5eec64c0269dcdb8d9a9a53dc4d64f87b9e0c19801d9321246a53b7eb5a7d1bc", size = 13331703 }, + { url = "https://files.pythonhosted.org/packages/12/f1/cefacac137f7bb7bfba57c50c478150fcd3c54aca72762ac2c05ce0532c1/onnxruntime-1.20.1-cp311-cp311-win32.whl", hash = "sha256:a19bc6e8c70e2485a1725b3d517a2319603acc14c1f1a017dda0afe6d4665b41", size = 9813977 }, + { url = "https://files.pythonhosted.org/packages/2c/2d/2d4d202c0bcfb3a4cc2b171abb9328672d7f91d7af9ea52572722c6d8d96/onnxruntime-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:8508887eb1c5f9537a4071768723ec7c30c28eb2518a00d0adcd32c89dea3221", size = 11329895 }, + { url = "https://files.pythonhosted.org/packages/e5/39/9335e0874f68f7d27103cbffc0e235e32e26759202df6085716375c078bb/onnxruntime-1.20.1-cp312-cp312-macosx_13_0_universal2.whl", hash = "sha256:22b0655e2bf4f2161d52706e31f517a0e54939dc393e92577df51808a7edc8c9", size = 31007580 }, + { url = "https://files.pythonhosted.org/packages/c5/9d/a42a84e10f1744dd27c6f2f9280cc3fb98f869dd19b7cd042e391ee2ab61/onnxruntime-1.20.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f56e898815963d6dc4ee1c35fc6c36506466eff6d16f3cb9848cea4e8c8172", size = 11952833 }, + { url = "https://files.pythonhosted.org/packages/47/42/2f71f5680834688a9c81becbe5c5bb996fd33eaed5c66ae0606c3b1d6a02/onnxruntime-1.20.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb71a814f66517a65628c9e4a2bb530a6edd2cd5d87ffa0af0f6f773a027d99e", size = 13333903 }, + { url = "https://files.pythonhosted.org/packages/c8/f1/aabfdf91d013320aa2fc46cf43c88ca0182860ff15df872b4552254a9680/onnxruntime-1.20.1-cp312-cp312-win32.whl", hash = "sha256:bd386cc9ee5f686ee8a75ba74037750aca55183085bf1941da8efcfe12d5b120", size = 9814562 }, + { url = "https://files.pythonhosted.org/packages/dd/80/76979e0b744307d488c79e41051117634b956612cc731f1028eb17ee7294/onnxruntime-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:19c2d843eb074f385e8bbb753a40df780511061a63f9def1b216bf53860223fb", size = 11331482 }, +] + +[[package]] +name = "openai" +version = "1.63.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e6/1c/11b520deb71f9ea54ced3c52cd6a5f7131215deba63ad07f23982e328141/openai-1.63.2.tar.gz", hash = "sha256:aeabeec984a7d2957b4928ceaa339e2ead19c61cfcf35ae62b7c363368d26360", size = 356902 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/64/db3462b358072387b8e93e6e6a38d3c741a17b4a84171ef01d6c85c63f25/openai-1.63.2-py3-none-any.whl", hash = "sha256:1f38b27b5a40814c2b7d8759ec78110df58c4a614c25f182809ca52b080ff4d4", size = 472282 }, +] + +[[package]] +name = "opencv-python" +version = "4.11.0.86" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/06/68c27a523103dad5837dc5b87e71285280c4f098c60e4fe8a8db6486ab09/opencv-python-4.11.0.86.tar.gz", hash = "sha256:03d60ccae62304860d232272e4a4fda93c39d595780cb40b161b310244b736a4", size = 95171956 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/4d/53b30a2a3ac1f75f65a59eb29cf2ee7207ce64867db47036ad61743d5a23/opencv_python-4.11.0.86-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:432f67c223f1dc2824f5e73cdfcd9db0efc8710647d4e813012195dc9122a52a", size = 37326322 }, + { url = "https://files.pythonhosted.org/packages/3b/84/0a67490741867eacdfa37bc18df96e08a9d579583b419010d7f3da8ff503/opencv_python-4.11.0.86-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:9d05ef13d23fe97f575153558653e2d6e87103995d54e6a35db3f282fe1f9c66", size = 56723197 }, + { url = "https://files.pythonhosted.org/packages/f3/bd/29c126788da65c1fb2b5fb621b7fed0ed5f9122aa22a0868c5e2c15c6d23/opencv_python-4.11.0.86-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b92ae2c8852208817e6776ba1ea0d6b1e0a1b5431e971a2a0ddd2a8cc398202", size = 42230439 }, + { url = "https://files.pythonhosted.org/packages/2c/8b/90eb44a40476fa0e71e05a0283947cfd74a5d36121a11d926ad6f3193cc4/opencv_python-4.11.0.86-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b02611523803495003bd87362db3e1d2a0454a6a63025dc6658a9830570aa0d", size = 62986597 }, + { url = "https://files.pythonhosted.org/packages/fb/d7/1d5941a9dde095468b288d989ff6539dd69cd429dbf1b9e839013d21b6f0/opencv_python-4.11.0.86-cp37-abi3-win32.whl", hash = "sha256:810549cb2a4aedaa84ad9a1c92fbfdfc14090e2749cedf2c1589ad8359aa169b", size = 29384337 }, + { url = "https://files.pythonhosted.org/packages/a4/7d/f1c30a92854540bf789e9cd5dde7ef49bbe63f855b85a2e6b3db8135c591/opencv_python-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:085ad9b77c18853ea66283e98affefe2de8cc4c1f43eda4c100cf9b2721142ec", size = 39488044 }, +] + +[[package]] +name = "orjson" +version = "3.10.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/5dea21763eeff8c1590076918a446ea3d6140743e0e36f58f369928ed0f4/orjson-3.10.15.tar.gz", hash = "sha256:05ca7fe452a2e9d8d9d706a2984c95b9c2ebc5db417ce0b7a49b91d50642a23e", size = 5282482 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/09/e5ff18ad009e6f97eb7edc5f67ef98b3ce0c189da9c3eaca1f9587cd4c61/orjson-3.10.15-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:552c883d03ad185f720d0c09583ebde257e41b9521b74ff40e08b7dec4559c04", size = 249532 }, + { url = "https://files.pythonhosted.org/packages/bd/b8/a75883301fe332bd433d9b0ded7d2bb706ccac679602c3516984f8814fb5/orjson-3.10.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e3e8d438d02e4854f70bfdc03a6bcdb697358dbaa6bcd19cbe24d24ece1f8", size = 125229 }, + { url = "https://files.pythonhosted.org/packages/83/4b/22f053e7a364cc9c685be203b1e40fc5f2b3f164a9b2284547504eec682e/orjson-3.10.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c2c79fa308e6edb0ffab0a31fd75a7841bf2a79a20ef08a3c6e3b26814c8ca8", size = 150148 }, + { url = "https://files.pythonhosted.org/packages/63/64/1b54fc75ca328b57dd810541a4035fe48c12a161d466e3cf5b11a8c25649/orjson-3.10.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cb85490aa6bf98abd20607ab5c8324c0acb48d6da7863a51be48505646c814", size = 139748 }, + { url = "https://files.pythonhosted.org/packages/5e/ff/ff0c5da781807bb0a5acd789d9a7fbcb57f7b0c6e1916595da1f5ce69f3c/orjson-3.10.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:763dadac05e4e9d2bc14938a45a2d0560549561287d41c465d3c58aec818b164", size = 154559 }, + { url = "https://files.pythonhosted.org/packages/4e/9a/11e2974383384ace8495810d4a2ebef5f55aacfc97b333b65e789c9d362d/orjson-3.10.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a330b9b4734f09a623f74a7490db713695e13b67c959713b78369f26b3dee6bf", size = 130349 }, + { url = "https://files.pythonhosted.org/packages/2d/c4/dd9583aea6aefee1b64d3aed13f51d2aadb014028bc929fe52936ec5091f/orjson-3.10.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a61a4622b7ff861f019974f73d8165be1bd9a0855e1cad18ee167acacabeb061", size = 138514 }, + { url = "https://files.pythonhosted.org/packages/53/3e/dcf1729230654f5c5594fc752de1f43dcf67e055ac0d300c8cdb1309269a/orjson-3.10.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:acd271247691574416b3228db667b84775c497b245fa275c6ab90dc1ffbbd2b3", size = 130940 }, + { url = "https://files.pythonhosted.org/packages/e8/2b/b9759fe704789937705c8a56a03f6c03e50dff7df87d65cba9a20fec5282/orjson-3.10.15-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4759b109c37f635aa5c5cc93a1b26927bfde24b254bcc0e1149a9fada253d2d", size = 414713 }, + { url = "https://files.pythonhosted.org/packages/a7/6b/b9dfdbd4b6e20a59238319eb203ae07c3f6abf07eef909169b7a37ae3bba/orjson-3.10.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9e992fd5cfb8b9f00bfad2fd7a05a4299db2bbe92e6440d9dd2fab27655b3182", size = 141028 }, + { url = "https://files.pythonhosted.org/packages/7c/b5/40f5bbea619c7caf75eb4d652a9821875a8ed04acc45fe3d3ef054ca69fb/orjson-3.10.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f95fb363d79366af56c3f26b71df40b9a583b07bbaaf5b317407c4d58497852e", size = 129715 }, + { url = "https://files.pythonhosted.org/packages/38/60/2272514061cbdf4d672edbca6e59c7e01cd1c706e881427d88f3c3e79761/orjson-3.10.15-cp310-cp310-win32.whl", hash = "sha256:f9875f5fea7492da8ec2444839dcc439b0ef298978f311103d0b7dfd775898ab", size = 142473 }, + { url = "https://files.pythonhosted.org/packages/11/5d/be1490ff7eafe7fef890eb4527cf5bcd8cfd6117f3efe42a3249ec847b60/orjson-3.10.15-cp310-cp310-win_amd64.whl", hash = "sha256:17085a6aa91e1cd70ca8533989a18b5433e15d29c574582f76f821737c8d5806", size = 133564 }, + { url = "https://files.pythonhosted.org/packages/7a/a2/21b25ce4a2c71dbb90948ee81bd7a42b4fbfc63162e57faf83157d5540ae/orjson-3.10.15-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c4cc83960ab79a4031f3119cc4b1a1c627a3dc09df125b27c4201dff2af7eaa6", size = 249533 }, + { url = "https://files.pythonhosted.org/packages/b2/85/2076fc12d8225698a51278009726750c9c65c846eda741e77e1761cfef33/orjson-3.10.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddbeef2481d895ab8be5185f2432c334d6dec1f5d1933a9c83014d188e102cef", size = 125230 }, + { url = "https://files.pythonhosted.org/packages/06/df/a85a7955f11274191eccf559e8481b2be74a7c6d43075d0a9506aa80284d/orjson-3.10.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9e590a0477b23ecd5b0ac865b1b907b01b3c5535f5e8a8f6ab0e503efb896334", size = 150148 }, + { url = "https://files.pythonhosted.org/packages/37/b3/94c55625a29b8767c0eed194cb000b3787e3c23b4cdd13be17bae6ccbb4b/orjson-3.10.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a6be38bd103d2fd9bdfa31c2720b23b5d47c6796bcb1d1b598e3924441b4298d", size = 139749 }, + { url = "https://files.pythonhosted.org/packages/53/ba/c608b1e719971e8ddac2379f290404c2e914cf8e976369bae3cad88768b1/orjson-3.10.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ff4f6edb1578960ed628a3b998fa54d78d9bb3e2eb2cfc5c2a09732431c678d0", size = 154558 }, + { url = "https://files.pythonhosted.org/packages/b2/c4/c1fb835bb23ad788a39aa9ebb8821d51b1c03588d9a9e4ca7de5b354fdd5/orjson-3.10.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0482b21d0462eddd67e7fce10b89e0b6ac56570424662b685a0d6fccf581e13", size = 130349 }, + { url = "https://files.pythonhosted.org/packages/78/14/bb2b48b26ab3c570b284eb2157d98c1ef331a8397f6c8bd983b270467f5c/orjson-3.10.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bb5cc3527036ae3d98b65e37b7986a918955f85332c1ee07f9d3f82f3a6899b5", size = 138513 }, + { url = "https://files.pythonhosted.org/packages/4a/97/d5b353a5fe532e92c46467aa37e637f81af8468aa894cd77d2ec8a12f99e/orjson-3.10.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d569c1c462912acdd119ccbf719cf7102ea2c67dd03b99edcb1a3048651ac96b", size = 130942 }, + { url = "https://files.pythonhosted.org/packages/b5/5d/a067bec55293cca48fea8b9928cfa84c623be0cce8141d47690e64a6ca12/orjson-3.10.15-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1e6d33efab6b71d67f22bf2962895d3dc6f82a6273a965fab762e64fa90dc399", size = 414717 }, + { url = "https://files.pythonhosted.org/packages/6f/9a/1485b8b05c6b4c4db172c438cf5db5dcfd10e72a9bc23c151a1137e763e0/orjson-3.10.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c33be3795e299f565681d69852ac8c1bc5c84863c0b0030b2b3468843be90388", size = 141033 }, + { url = "https://files.pythonhosted.org/packages/f8/d2/fc67523656e43a0c7eaeae9007c8b02e86076b15d591e9be11554d3d3138/orjson-3.10.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:eea80037b9fae5339b214f59308ef0589fc06dc870578b7cce6d71eb2096764c", size = 129720 }, + { url = "https://files.pythonhosted.org/packages/79/42/f58c7bd4e5b54da2ce2ef0331a39ccbbaa7699b7f70206fbf06737c9ed7d/orjson-3.10.15-cp311-cp311-win32.whl", hash = "sha256:d5ac11b659fd798228a7adba3e37c010e0152b78b1982897020a8e019a94882e", size = 142473 }, + { url = "https://files.pythonhosted.org/packages/00/f8/bb60a4644287a544ec81df1699d5b965776bc9848d9029d9f9b3402ac8bb/orjson-3.10.15-cp311-cp311-win_amd64.whl", hash = "sha256:cf45e0214c593660339ef63e875f32ddd5aa3b4adc15e662cdb80dc49e194f8e", size = 133570 }, + { url = "https://files.pythonhosted.org/packages/66/85/22fe737188905a71afcc4bf7cc4c79cd7f5bbe9ed1fe0aac4ce4c33edc30/orjson-3.10.15-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9d11c0714fc85bfcf36ada1179400862da3288fc785c30e8297844c867d7505a", size = 249504 }, + { url = "https://files.pythonhosted.org/packages/48/b7/2622b29f3afebe938a0a9037e184660379797d5fd5234e5998345d7a5b43/orjson-3.10.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dba5a1e85d554e3897fa9fe6fbcff2ed32d55008973ec9a2b992bd9a65d2352d", size = 125080 }, + { url = "https://files.pythonhosted.org/packages/ce/8f/0b72a48f4403d0b88b2a41450c535b3e8989e8a2d7800659a967efc7c115/orjson-3.10.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7723ad949a0ea502df656948ddd8b392780a5beaa4c3b5f97e525191b102fff0", size = 150121 }, + { url = "https://files.pythonhosted.org/packages/06/ec/acb1a20cd49edb2000be5a0404cd43e3c8aad219f376ac8c60b870518c03/orjson-3.10.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6fd9bc64421e9fe9bd88039e7ce8e58d4fead67ca88e3a4014b143cec7684fd4", size = 139796 }, + { url = "https://files.pythonhosted.org/packages/33/e1/f7840a2ea852114b23a52a1c0b2bea0a1ea22236efbcdb876402d799c423/orjson-3.10.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dadba0e7b6594216c214ef7894c4bd5f08d7c0135f4dd0145600be4fbcc16767", size = 154636 }, + { url = "https://files.pythonhosted.org/packages/fa/da/31543337febd043b8fa80a3b67de627669b88c7b128d9ad4cc2ece005b7a/orjson-3.10.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b48f59114fe318f33bbaee8ebeda696d8ccc94c9e90bc27dbe72153094e26f41", size = 130621 }, + { url = "https://files.pythonhosted.org/packages/ed/78/66115dc9afbc22496530d2139f2f4455698be444c7c2475cb48f657cefc9/orjson-3.10.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:035fb83585e0f15e076759b6fedaf0abb460d1765b6a36f48018a52858443514", size = 138516 }, + { url = "https://files.pythonhosted.org/packages/22/84/cd4f5fb5427ffcf823140957a47503076184cb1ce15bcc1165125c26c46c/orjson-3.10.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d13b7fe322d75bf84464b075eafd8e7dd9eae05649aa2a5354cfa32f43c59f17", size = 130762 }, + { url = "https://files.pythonhosted.org/packages/93/1f/67596b711ba9f56dd75d73b60089c5c92057f1130bb3a25a0f53fb9a583b/orjson-3.10.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7066b74f9f259849629e0d04db6609db4cf5b973248f455ba5d3bd58a4daaa5b", size = 414700 }, + { url = "https://files.pythonhosted.org/packages/7c/0c/6a3b3271b46443d90efb713c3e4fe83fa8cd71cda0d11a0f69a03f437c6e/orjson-3.10.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:88dc3f65a026bd3175eb157fea994fca6ac7c4c8579fc5a86fc2114ad05705b7", size = 141077 }, + { url = "https://files.pythonhosted.org/packages/3b/9b/33c58e0bfc788995eccd0d525ecd6b84b40d7ed182dd0751cd4c1322ac62/orjson-3.10.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b342567e5465bd99faa559507fe45e33fc76b9fb868a63f1642c6bc0735ad02a", size = 129898 }, + { url = "https://files.pythonhosted.org/packages/01/c1/d577ecd2e9fa393366a1ea0a9267f6510d86e6c4bb1cdfb9877104cac44c/orjson-3.10.15-cp312-cp312-win32.whl", hash = "sha256:0a4f27ea5617828e6b58922fdbec67b0aa4bb844e2d363b9244c47fa2180e665", size = 142566 }, + { url = "https://files.pythonhosted.org/packages/ed/eb/a85317ee1732d1034b92d56f89f1de4d7bf7904f5c8fb9dcdd5b1c83917f/orjson-3.10.15-cp312-cp312-win_amd64.whl", hash = "sha256:ef5b87e7aa9545ddadd2309efe6824bd3dd64ac101c15dae0f2f597911d46eaa", size = 133732 }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "pandas" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/70/c853aec59839bceed032d52010ff5f1b8d87dc3114b762e4ba2727661a3b/pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5", size = 12580827 }, + { url = "https://files.pythonhosted.org/packages/99/f2/c4527768739ffa4469b2b4fff05aa3768a478aed89a2f271a79a40eee984/pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348", size = 11303897 }, + { url = "https://files.pythonhosted.org/packages/ed/12/86c1747ea27989d7a4064f806ce2bae2c6d575b950be087837bdfcabacc9/pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed", size = 66480908 }, + { url = "https://files.pythonhosted.org/packages/44/50/7db2cd5e6373ae796f0ddad3675268c8d59fb6076e66f0c339d61cea886b/pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57", size = 13064210 }, + { url = "https://files.pythonhosted.org/packages/61/61/a89015a6d5536cb0d6c3ba02cebed51a95538cf83472975275e28ebf7d0c/pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42", size = 16754292 }, + { url = "https://files.pythonhosted.org/packages/ce/0d/4cc7b69ce37fac07645a94e1d4b0880b15999494372c1523508511b09e40/pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f", size = 14416379 }, + { url = "https://files.pythonhosted.org/packages/31/9e/6ebb433de864a6cd45716af52a4d7a8c3c9aaf3a98368e61db9e69e69a9c/pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645", size = 11598471 }, + { url = "https://files.pythonhosted.org/packages/a8/44/d9502bf0ed197ba9bf1103c9867d5904ddcaf869e52329787fc54ed70cc8/pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039", size = 12602222 }, + { url = "https://files.pythonhosted.org/packages/52/11/9eac327a38834f162b8250aab32a6781339c69afe7574368fffe46387edf/pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd", size = 11321274 }, + { url = "https://files.pythonhosted.org/packages/45/fb/c4beeb084718598ba19aa9f5abbc8aed8b42f90930da861fcb1acdb54c3a/pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698", size = 15579836 }, + { url = "https://files.pythonhosted.org/packages/cd/5f/4dba1d39bb9c38d574a9a22548c540177f78ea47b32f99c0ff2ec499fac5/pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc", size = 13058505 }, + { url = "https://files.pythonhosted.org/packages/b9/57/708135b90391995361636634df1f1130d03ba456e95bcf576fada459115a/pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3", size = 16744420 }, + { url = "https://files.pythonhosted.org/packages/86/4a/03ed6b7ee323cf30404265c284cee9c65c56a212e0a08d9ee06984ba2240/pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32", size = 14440457 }, + { url = "https://files.pythonhosted.org/packages/ed/8c/87ddf1fcb55d11f9f847e3c69bb1c6f8e46e2f40ab1a2d2abadb2401b007/pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5", size = 11617166 }, + { url = "https://files.pythonhosted.org/packages/17/a3/fb2734118db0af37ea7433f57f722c0a56687e14b14690edff0cdb4b7e58/pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9", size = 12529893 }, + { url = "https://files.pythonhosted.org/packages/e1/0c/ad295fd74bfac85358fd579e271cded3ac969de81f62dd0142c426b9da91/pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4", size = 11363475 }, + { url = "https://files.pythonhosted.org/packages/c6/2a/4bba3f03f7d07207481fed47f5b35f556c7441acddc368ec43d6643c5777/pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3", size = 15188645 }, + { url = "https://files.pythonhosted.org/packages/38/f8/d8fddee9ed0d0c0f4a2132c1dfcf0e3e53265055da8df952a53e7eaf178c/pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319", size = 12739445 }, + { url = "https://files.pythonhosted.org/packages/20/e8/45a05d9c39d2cea61ab175dbe6a2de1d05b679e8de2011da4ee190d7e748/pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8", size = 16359235 }, + { url = "https://files.pythonhosted.org/packages/1d/99/617d07a6a5e429ff90c90da64d428516605a1ec7d7bea494235e1c3882de/pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a", size = 14056756 }, + { url = "https://files.pythonhosted.org/packages/29/d4/1244ab8edf173a10fd601f7e13b9566c1b525c4f365d6bee918e68381889/pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", size = 11504248 }, +] + +[[package]] +name = "pdf2u" +version = "0.0.4" +source = { editable = "." } +dependencies = [ + { name = "bitstring" }, + { name = "configargparse" }, + { name = "httpx", extra = ["socks"] }, + { name = "huggingface-hub" }, + { name = "msgpack" }, + { name = "numpy" }, + { name = "onnx" }, + { name = "onnxruntime" }, + { name = "openai" }, + { name = "opencv-python" }, + { name = "orjson" }, + { name = "pdfminer-six" }, + { name = "peewee" }, + { name = "pymupdf" }, + { name = "rich" }, + { name = "toml" }, + { name = "tqdm" }, + { name = "typer" }, + { name = "xsdata", extra = ["cli", "lxml", "soap"] }, +] + +[package.optional-dependencies] +gui = [ + { name = "pypdf2" }, + { name = "streamlit" }, + { name = "streamlit-pdf-viewer" }, +] + +[package.dev-dependencies] +dev = [ + { name = "bump-my-version" }, + { name = "coverage" }, + { name = "fonttools" }, + { name = "git-cliff" }, + { name = "mypy" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-sugar" }, + { name = "ruff" }, + { name = "typos" }, +] + +[package.metadata] +requires-dist = [ + { name = "bitstring", specifier = ">=4.3.0" }, + { name = "configargparse", specifier = ">=1.7" }, + { name = "httpx", extras = ["socks"], specifier = ">=0.27.0" }, + { name = "huggingface-hub", specifier = ">=0.27.0" }, + { name = "msgpack", specifier = ">=1.1.0" }, + { name = "numpy", specifier = ">=2.0.2" }, + { name = "onnx", specifier = ">=1.17.0" }, + { name = "onnxruntime", specifier = ">=1.16.1" }, + { name = "openai", specifier = ">=1.59.3" }, + { name = "opencv-python", specifier = ">=4.10.0.84" }, + { name = "orjson", specifier = ">=3.10.14" }, + { name = "pdfminer-six", specifier = ">=20240706" }, + { name = "peewee", specifier = ">=3.17.8" }, + { name = "pymupdf", specifier = "==1.24.5" }, + { name = "pypdf2", marker = "extra == 'gui'", specifier = ">=3.0.1" }, + { name = "rich", specifier = ">=13.9.4" }, + { name = "streamlit", marker = "extra == 'gui'", specifier = ">=1.42.2" }, + { name = "streamlit-pdf-viewer", marker = "extra == 'gui'", specifier = ">=0.0.21" }, + { name = "toml", specifier = ">=0.10.2" }, + { name = "tqdm", specifier = ">=4.67.1" }, + { name = "typer", specifier = ">=0.15.1" }, + { name = "xsdata", extras = ["cli", "lxml", "soap"], specifier = ">=24.12" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "bump-my-version", specifier = ">=0.28.0" }, + { name = "coverage", specifier = ">=7.6.1" }, + { name = "fonttools", specifier = ">=4.56.0" }, + { name = "git-cliff", specifier = ">=2.6.1" }, + { name = "mypy", specifier = ">=1.11.2" }, + { name = "pre-commit", specifier = ">=3.8.0" }, + { name = "pytest", specifier = ">=8.3.2" }, + { name = "pytest-sugar", specifier = ">=1.0.0" }, + { name = "ruff", specifier = ">=0.6.3" }, + { name = "typos", specifier = ">=1.26.8" }, +] + +[[package]] +name = "pdfminer-six" +version = "20240706" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "charset-normalizer" }, + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/37/63cb918ffa21412dd5d54e32e190e69bfc340f3d6aa072ad740bec9386bb/pdfminer.six-20240706.tar.gz", hash = "sha256:c631a46d5da957a9ffe4460c5dce21e8431dabb615fee5f9f4400603a58d95a6", size = 7363505 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/7d/44d6b90e5a293d3a975cefdc4e12a932ebba814995b2a07e37e599dd27c6/pdfminer.six-20240706-py3-none-any.whl", hash = "sha256:f4f70e74174b4b3542fcb8406a210b6e2e27cd0f0b5fd04534a8cc0d8951e38c", size = 5615414 }, +] + +[[package]] +name = "peewee" +version = "3.17.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/09/4393bd378e70b7fc3163ee83353cc27bb520010a5c2b3c924121e7e7e068/peewee-3.17.9.tar.gz", hash = "sha256:fe15cd001758e324c8e3ca8c8ed900e7397c2907291789e1efc383e66b9bc7a8", size = 3026085 } + +[[package]] +name = "pillow" +version = "11.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/af/c097e544e7bd278333db77933e535098c259609c4eb3b85381109602fb5b/pillow-11.1.0.tar.gz", hash = "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20", size = 46742715 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/1c/2dcea34ac3d7bc96a1fd1bd0a6e06a57c67167fec2cff8d95d88229a8817/pillow-11.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:e1abe69aca89514737465752b4bcaf8016de61b3be1397a8fc260ba33321b3a8", size = 3229983 }, + { url = "https://files.pythonhosted.org/packages/14/ca/6bec3df25e4c88432681de94a3531cc738bd85dea6c7aa6ab6f81ad8bd11/pillow-11.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c640e5a06869c75994624551f45e5506e4256562ead981cce820d5ab39ae2192", size = 3101831 }, + { url = "https://files.pythonhosted.org/packages/d4/2c/668e18e5521e46eb9667b09e501d8e07049eb5bfe39d56be0724a43117e6/pillow-11.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a07dba04c5e22824816b2615ad7a7484432d7f540e6fa86af60d2de57b0fcee2", size = 4314074 }, + { url = "https://files.pythonhosted.org/packages/02/80/79f99b714f0fc25f6a8499ecfd1f810df12aec170ea1e32a4f75746051ce/pillow-11.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e267b0ed063341f3e60acd25c05200df4193e15a4a5807075cd71225a2386e26", size = 4394933 }, + { url = "https://files.pythonhosted.org/packages/81/aa/8d4ad25dc11fd10a2001d5b8a80fdc0e564ac33b293bdfe04ed387e0fd95/pillow-11.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bd165131fd51697e22421d0e467997ad31621b74bfc0b75956608cb2906dda07", size = 4353349 }, + { url = "https://files.pythonhosted.org/packages/84/7a/cd0c3eaf4a28cb2a74bdd19129f7726277a7f30c4f8424cd27a62987d864/pillow-11.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:abc56501c3fd148d60659aae0af6ddc149660469082859fa7b066a298bde9482", size = 4476532 }, + { url = "https://files.pythonhosted.org/packages/8f/8b/a907fdd3ae8f01c7670dfb1499c53c28e217c338b47a813af8d815e7ce97/pillow-11.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:54ce1c9a16a9561b6d6d8cb30089ab1e5eb66918cb47d457bd996ef34182922e", size = 4279789 }, + { url = "https://files.pythonhosted.org/packages/6f/9a/9f139d9e8cccd661c3efbf6898967a9a337eb2e9be2b454ba0a09533100d/pillow-11.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:73ddde795ee9b06257dac5ad42fcb07f3b9b813f8c1f7f870f402f4dc54b5269", size = 4413131 }, + { url = "https://files.pythonhosted.org/packages/a8/68/0d8d461f42a3f37432203c8e6df94da10ac8081b6d35af1c203bf3111088/pillow-11.1.0-cp310-cp310-win32.whl", hash = "sha256:3a5fe20a7b66e8135d7fd617b13272626a28278d0e578c98720d9ba4b2439d49", size = 2291213 }, + { url = "https://files.pythonhosted.org/packages/14/81/d0dff759a74ba87715509af9f6cb21fa21d93b02b3316ed43bda83664db9/pillow-11.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:b6123aa4a59d75f06e9dd3dac5bf8bc9aa383121bb3dd9a7a612e05eabc9961a", size = 2625725 }, + { url = "https://files.pythonhosted.org/packages/ce/1f/8d50c096a1d58ef0584ddc37e6f602828515219e9d2428e14ce50f5ecad1/pillow-11.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:a76da0a31da6fcae4210aa94fd779c65c75786bc9af06289cd1c184451ef7a65", size = 2375213 }, + { url = "https://files.pythonhosted.org/packages/dd/d6/2000bfd8d5414fb70cbbe52c8332f2283ff30ed66a9cde42716c8ecbe22c/pillow-11.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e06695e0326d05b06833b40b7ef477e475d0b1ba3a6d27da1bb48c23209bf457", size = 3229968 }, + { url = "https://files.pythonhosted.org/packages/d9/45/3fe487010dd9ce0a06adf9b8ff4f273cc0a44536e234b0fad3532a42c15b/pillow-11.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96f82000e12f23e4f29346e42702b6ed9a2f2fea34a740dd5ffffcc8c539eb35", size = 3101806 }, + { url = "https://files.pythonhosted.org/packages/e3/72/776b3629c47d9d5f1c160113158a7a7ad177688d3a1159cd3b62ded5a33a/pillow-11.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3cd561ded2cf2bbae44d4605837221b987c216cff94f49dfeed63488bb228d2", size = 4322283 }, + { url = "https://files.pythonhosted.org/packages/e4/c2/e25199e7e4e71d64eeb869f5b72c7ddec70e0a87926398785ab944d92375/pillow-11.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f189805c8be5ca5add39e6f899e6ce2ed824e65fb45f3c28cb2841911da19070", size = 4402945 }, + { url = "https://files.pythonhosted.org/packages/c1/ed/51d6136c9d5911f78632b1b86c45241c712c5a80ed7fa7f9120a5dff1eba/pillow-11.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:dd0052e9db3474df30433f83a71b9b23bd9e4ef1de13d92df21a52c0303b8ab6", size = 4361228 }, + { url = "https://files.pythonhosted.org/packages/48/a4/fbfe9d5581d7b111b28f1d8c2762dee92e9821bb209af9fa83c940e507a0/pillow-11.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:837060a8599b8f5d402e97197d4924f05a2e0d68756998345c829c33186217b1", size = 4484021 }, + { url = "https://files.pythonhosted.org/packages/39/db/0b3c1a5018117f3c1d4df671fb8e47d08937f27519e8614bbe86153b65a5/pillow-11.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aa8dd43daa836b9a8128dbe7d923423e5ad86f50a7a14dc688194b7be5c0dea2", size = 4287449 }, + { url = "https://files.pythonhosted.org/packages/d9/58/bc128da7fea8c89fc85e09f773c4901e95b5936000e6f303222490c052f3/pillow-11.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0a2f91f8a8b367e7a57c6e91cd25af510168091fb89ec5146003e424e1558a96", size = 4419972 }, + { url = "https://files.pythonhosted.org/packages/5f/bb/58f34379bde9fe197f51841c5bbe8830c28bbb6d3801f16a83b8f2ad37df/pillow-11.1.0-cp311-cp311-win32.whl", hash = "sha256:c12fc111ef090845de2bb15009372175d76ac99969bdf31e2ce9b42e4b8cd88f", size = 2291201 }, + { url = "https://files.pythonhosted.org/packages/3a/c6/fce9255272bcf0c39e15abd2f8fd8429a954cf344469eaceb9d0d1366913/pillow-11.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbd43429d0d7ed6533b25fc993861b8fd512c42d04514a0dd6337fb3ccf22761", size = 2625686 }, + { url = "https://files.pythonhosted.org/packages/c8/52/8ba066d569d932365509054859f74f2a9abee273edcef5cd75e4bc3e831e/pillow-11.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:f7955ecf5609dee9442cbface754f2c6e541d9e6eda87fad7f7a989b0bdb9d71", size = 2375194 }, + { url = "https://files.pythonhosted.org/packages/95/20/9ce6ed62c91c073fcaa23d216e68289e19d95fb8188b9fb7a63d36771db8/pillow-11.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2062ffb1d36544d42fcaa277b069c88b01bb7298f4efa06731a7fd6cc290b81a", size = 3226818 }, + { url = "https://files.pythonhosted.org/packages/b9/d8/f6004d98579a2596c098d1e30d10b248798cceff82d2b77aa914875bfea1/pillow-11.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a85b653980faad27e88b141348707ceeef8a1186f75ecc600c395dcac19f385b", size = 3101662 }, + { url = "https://files.pythonhosted.org/packages/08/d9/892e705f90051c7a2574d9f24579c9e100c828700d78a63239676f960b74/pillow-11.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9409c080586d1f683df3f184f20e36fb647f2e0bc3988094d4fd8c9f4eb1b3b3", size = 4329317 }, + { url = "https://files.pythonhosted.org/packages/8c/aa/7f29711f26680eab0bcd3ecdd6d23ed6bce180d82e3f6380fb7ae35fcf3b/pillow-11.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fdadc077553621911f27ce206ffcbec7d3f8d7b50e0da39f10997e8e2bb7f6a", size = 4412999 }, + { url = "https://files.pythonhosted.org/packages/c8/c4/8f0fe3b9e0f7196f6d0bbb151f9fba323d72a41da068610c4c960b16632a/pillow-11.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:93a18841d09bcdd774dcdc308e4537e1f867b3dec059c131fde0327899734aa1", size = 4368819 }, + { url = "https://files.pythonhosted.org/packages/38/0d/84200ed6a871ce386ddc82904bfadc0c6b28b0c0ec78176871a4679e40b3/pillow-11.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9aa9aeddeed452b2f616ff5507459e7bab436916ccb10961c4a382cd3e03f47f", size = 4496081 }, + { url = "https://files.pythonhosted.org/packages/84/9c/9bcd66f714d7e25b64118e3952d52841a4babc6d97b6d28e2261c52045d4/pillow-11.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3cdcdb0b896e981678eee140d882b70092dac83ac1cdf6b3a60e2216a73f2b91", size = 4296513 }, + { url = "https://files.pythonhosted.org/packages/db/61/ada2a226e22da011b45f7104c95ebda1b63dcbb0c378ad0f7c2a710f8fd2/pillow-11.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36ba10b9cb413e7c7dfa3e189aba252deee0602c86c309799da5a74009ac7a1c", size = 4431298 }, + { url = "https://files.pythonhosted.org/packages/e7/c4/fc6e86750523f367923522014b821c11ebc5ad402e659d8c9d09b3c9d70c/pillow-11.1.0-cp312-cp312-win32.whl", hash = "sha256:cfd5cd998c2e36a862d0e27b2df63237e67273f2fc78f47445b14e73a810e7e6", size = 2291630 }, + { url = "https://files.pythonhosted.org/packages/08/5c/2104299949b9d504baf3f4d35f73dbd14ef31bbd1ddc2c1b66a5b7dfda44/pillow-11.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a697cd8ba0383bba3d2d3ada02b34ed268cb548b369943cd349007730c92bddf", size = 2626369 }, + { url = "https://files.pythonhosted.org/packages/37/f3/9b18362206b244167c958984b57c7f70a0289bfb59a530dd8af5f699b910/pillow-11.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:4dd43a78897793f60766563969442020e90eb7847463eca901e41ba186a7d4a5", size = 2375240 }, + { url = "https://files.pythonhosted.org/packages/fa/c5/389961578fb677b8b3244fcd934f720ed25a148b9a5cc81c91bdf59d8588/pillow-11.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8c730dc3a83e5ac137fbc92dfcfe1511ce3b2b5d7578315b63dbbb76f7f51d90", size = 3198345 }, + { url = "https://files.pythonhosted.org/packages/c4/fa/803c0e50ffee74d4b965229e816af55276eac1d5806712de86f9371858fd/pillow-11.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:7d33d2fae0e8b170b6a6c57400e077412240f6f5bb2a342cf1ee512a787942bb", size = 3072938 }, + { url = "https://files.pythonhosted.org/packages/dc/67/2a3a5f8012b5d8c63fe53958ba906c1b1d0482ebed5618057ef4d22f8076/pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8d65b38173085f24bc07f8b6c505cbb7418009fa1a1fcb111b1f4961814a442", size = 3400049 }, + { url = "https://files.pythonhosted.org/packages/e5/a0/514f0d317446c98c478d1872497eb92e7cde67003fed74f696441e647446/pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:015c6e863faa4779251436db398ae75051469f7c903b043a48f078e437656f83", size = 3422431 }, + { url = "https://files.pythonhosted.org/packages/cd/00/20f40a935514037b7d3f87adfc87d2c538430ea625b63b3af8c3f5578e72/pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d44ff19eea13ae4acdaaab0179fa68c0c6f2f45d66a4d8ec1eda7d6cecbcc15f", size = 3446208 }, + { url = "https://files.pythonhosted.org/packages/28/3c/7de681727963043e093c72e6c3348411b0185eab3263100d4490234ba2f6/pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d3d8da4a631471dfaf94c10c85f5277b1f8e42ac42bade1ac67da4b4a7359b73", size = 3509746 }, + { url = "https://files.pythonhosted.org/packages/41/67/936f9814bdd74b2dfd4822f1f7725ab5d8ff4103919a1664eb4874c58b2f/pillow-11.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4637b88343166249fe8aa94e7c4a62a180c4b3898283bb5d3d2fd5fe10d8e4e0", size = 2626353 }, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "pre-commit" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/13/b62d075317d8686071eb843f0bb1f195eb332f48869d3c31a4c6f1e063ac/pre_commit-4.1.0.tar.gz", hash = "sha256:ae3f018575a588e30dfddfab9a05448bfbd6b73d78709617b5a2b853549716d4", size = 193330 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/b3/df14c580d82b9627d173ceea305ba898dca135feb360b6d84019d0803d3b/pre_commit-4.1.0-py2.py3-none-any.whl", hash = "sha256:d29e7cb346295bcc1cc75fc3e92e343495e3ea0196c9ec6ba53f49f10ab6ae7b", size = 220560 }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.50" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/e1/bd15cb8ffdcfeeb2bdc215de3c3cffca11408d829e4b8416dcfe71ba8854/prompt_toolkit-3.0.50.tar.gz", hash = "sha256:544748f3860a2623ca5cd6d2795e7a14f3d0e1c3c9728359013f79877fc89bab", size = 429087 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/ea/d836f008d33151c7a1f62caf3d8dd782e4d15f6a43897f64480c2b8de2ad/prompt_toolkit-3.0.50-py3-none-any.whl", hash = "sha256:9b6427eb19e479d98acff65196a307c555eb567989e6d88ebbb1b509d9779198", size = 387816 }, +] + +[[package]] +name = "protobuf" +version = "5.29.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/d1/e0a911544ca9993e0f17ce6d3cc0932752356c1b0a834397f28e63479344/protobuf-5.29.3.tar.gz", hash = "sha256:5da0f41edaf117bde316404bad1a486cb4ededf8e4a54891296f648e8e076620", size = 424945 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/7a/1e38f3cafa022f477ca0f57a1f49962f21ad25850c3ca0acd3b9d0091518/protobuf-5.29.3-cp310-abi3-win32.whl", hash = "sha256:3ea51771449e1035f26069c4c7fd51fba990d07bc55ba80701c78f886bf9c888", size = 422708 }, + { url = "https://files.pythonhosted.org/packages/61/fa/aae8e10512b83de633f2646506a6d835b151edf4b30d18d73afd01447253/protobuf-5.29.3-cp310-abi3-win_amd64.whl", hash = "sha256:a4fa6f80816a9a0678429e84973f2f98cbc218cca434abe8db2ad0bffc98503a", size = 434508 }, + { url = "https://files.pythonhosted.org/packages/dd/04/3eaedc2ba17a088961d0e3bd396eac764450f431621b58a04ce898acd126/protobuf-5.29.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a8434404bbf139aa9e1300dbf989667a83d42ddda9153d8ab76e0d5dcaca484e", size = 417825 }, + { url = "https://files.pythonhosted.org/packages/4f/06/7c467744d23c3979ce250397e26d8ad8eeb2bea7b18ca12ad58313c1b8d5/protobuf-5.29.3-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:daaf63f70f25e8689c072cfad4334ca0ac1d1e05a92fc15c54eb9cf23c3efd84", size = 319573 }, + { url = "https://files.pythonhosted.org/packages/a8/45/2ebbde52ad2be18d3675b6bee50e68cd73c9e0654de77d595540b5129df8/protobuf-5.29.3-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:c027e08a08be10b67c06bf2370b99c811c466398c357e615ca88c91c07f0910f", size = 319672 }, + { url = "https://files.pythonhosted.org/packages/fd/b2/ab07b09e0f6d143dfb839693aa05765257bceaa13d03bf1a696b78323e7a/protobuf-5.29.3-py3-none-any.whl", hash = "sha256:0a18ed4a24198528f2333802eb075e59dea9d679ab7a6c5efb017a59004d849f", size = 172550 }, +] + +[[package]] +name = "pyarrow" +version = "19.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7f/09/a9046344212690f0632b9c709f9bf18506522feb333c894d0de81d62341a/pyarrow-19.0.1.tar.gz", hash = "sha256:3bf266b485df66a400f282ac0b6d1b500b9d2ae73314a153dbe97d6d5cc8a99e", size = 1129437 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/01/b23b514d86b839956238d3f8ef206fd2728eee87ff1b8ce150a5678d9721/pyarrow-19.0.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:fc28912a2dc924dddc2087679cc8b7263accc71b9ff025a1362b004711661a69", size = 30688914 }, + { url = "https://files.pythonhosted.org/packages/c6/68/218ff7cf4a0652a933e5f2ed11274f724dd43b9813cb18dd72c0a35226a2/pyarrow-19.0.1-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:fca15aabbe9b8355800d923cc2e82c8ef514af321e18b437c3d782aa884eaeec", size = 32102866 }, + { url = "https://files.pythonhosted.org/packages/98/01/c295050d183014f4a2eb796d7d2bbfa04b6cccde7258bb68aacf6f18779b/pyarrow-19.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad76aef7f5f7e4a757fddcdcf010a8290958f09e3470ea458c80d26f4316ae89", size = 41147682 }, + { url = "https://files.pythonhosted.org/packages/40/17/a6c3db0b5f3678f33bbb552d2acbc16def67f89a72955b67b0109af23eb0/pyarrow-19.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d03c9d6f2a3dffbd62671ca070f13fc527bb1867b4ec2b98c7eeed381d4f389a", size = 42179192 }, + { url = "https://files.pythonhosted.org/packages/cf/75/c7c8e599300d8cebb6cb339014800e1c720c9db2a3fcb66aa64ec84bac72/pyarrow-19.0.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:65cf9feebab489b19cdfcfe4aa82f62147218558d8d3f0fc1e9dea0ab8e7905a", size = 40517272 }, + { url = "https://files.pythonhosted.org/packages/ef/c9/68ab123ee1528699c4d5055f645ecd1dd68ff93e4699527249d02f55afeb/pyarrow-19.0.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:41f9706fbe505e0abc10e84bf3a906a1338905cbbcf1177b71486b03e6ea6608", size = 42069036 }, + { url = "https://files.pythonhosted.org/packages/54/e3/d5cfd7654084e6c0d9c3ce949e5d9e0ccad569ae1e2d5a68a3ec03b2be89/pyarrow-19.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:c6cb2335a411b713fdf1e82a752162f72d4a7b5dbc588e32aa18383318b05866", size = 25277951 }, + { url = "https://files.pythonhosted.org/packages/a0/55/f1a8d838ec07fe3ca53edbe76f782df7b9aafd4417080eebf0b42aab0c52/pyarrow-19.0.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:cc55d71898ea30dc95900297d191377caba257612f384207fe9f8293b5850f90", size = 30713987 }, + { url = "https://files.pythonhosted.org/packages/13/12/428861540bb54c98a140ae858a11f71d041ef9e501e6b7eb965ca7909505/pyarrow-19.0.1-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:7a544ec12de66769612b2d6988c36adc96fb9767ecc8ee0a4d270b10b1c51e00", size = 32135613 }, + { url = "https://files.pythonhosted.org/packages/2f/8a/23d7cc5ae2066c6c736bce1db8ea7bc9ac3ef97ac7e1c1667706c764d2d9/pyarrow-19.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0148bb4fc158bfbc3d6dfe5001d93ebeed253793fff4435167f6ce1dc4bddeae", size = 41149147 }, + { url = "https://files.pythonhosted.org/packages/a2/7a/845d151bb81a892dfb368bf11db584cf8b216963ccce40a5cf50a2492a18/pyarrow-19.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f24faab6ed18f216a37870d8c5623f9c044566d75ec586ef884e13a02a9d62c5", size = 42178045 }, + { url = "https://files.pythonhosted.org/packages/a7/31/e7282d79a70816132cf6cae7e378adfccce9ae10352d21c2fecf9d9756dd/pyarrow-19.0.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:4982f8e2b7afd6dae8608d70ba5bd91699077323f812a0448d8b7abdff6cb5d3", size = 40532998 }, + { url = "https://files.pythonhosted.org/packages/b8/82/20f3c290d6e705e2ee9c1fa1d5a0869365ee477e1788073d8b548da8b64c/pyarrow-19.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:49a3aecb62c1be1d822f8bf629226d4a96418228a42f5b40835c1f10d42e4db6", size = 42084055 }, + { url = "https://files.pythonhosted.org/packages/ff/77/e62aebd343238863f2c9f080ad2ef6ace25c919c6ab383436b5b81cbeef7/pyarrow-19.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:008a4009efdb4ea3d2e18f05cd31f9d43c388aad29c636112c2966605ba33466", size = 25283133 }, + { url = "https://files.pythonhosted.org/packages/78/b4/94e828704b050e723f67d67c3535cf7076c7432cd4cf046e4bb3b96a9c9d/pyarrow-19.0.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:80b2ad2b193e7d19e81008a96e313fbd53157945c7be9ac65f44f8937a55427b", size = 30670749 }, + { url = "https://files.pythonhosted.org/packages/7e/3b/4692965e04bb1df55e2c314c4296f1eb12b4f3052d4cf43d29e076aedf66/pyarrow-19.0.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:ee8dec072569f43835932a3b10c55973593abc00936c202707a4ad06af7cb294", size = 32128007 }, + { url = "https://files.pythonhosted.org/packages/22/f7/2239af706252c6582a5635c35caa17cb4d401cd74a87821ef702e3888957/pyarrow-19.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d5d1ec7ec5324b98887bdc006f4d2ce534e10e60f7ad995e7875ffa0ff9cb14", size = 41144566 }, + { url = "https://files.pythonhosted.org/packages/fb/e3/c9661b2b2849cfefddd9fd65b64e093594b231b472de08ff658f76c732b2/pyarrow-19.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3ad4c0eb4e2a9aeb990af6c09e6fa0b195c8c0e7b272ecc8d4d2b6574809d34", size = 42202991 }, + { url = "https://files.pythonhosted.org/packages/fe/4f/a2c0ed309167ef436674782dfee4a124570ba64299c551e38d3fdaf0a17b/pyarrow-19.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:d383591f3dcbe545f6cc62daaef9c7cdfe0dff0fb9e1c8121101cabe9098cfa6", size = 40507986 }, + { url = "https://files.pythonhosted.org/packages/27/2e/29bb28a7102a6f71026a9d70d1d61df926887e36ec797f2e6acfd2dd3867/pyarrow-19.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b4c4156a625f1e35d6c0b2132635a237708944eb41df5fbe7d50f20d20c17832", size = 42087026 }, + { url = "https://files.pythonhosted.org/packages/16/33/2a67c0f783251106aeeee516f4806161e7b481f7d744d0d643d2f30230a5/pyarrow-19.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:5bd1618ae5e5476b7654c7b55a6364ae87686d4724538c24185bbb2952679960", size = 25250108 }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, +] + +[[package]] +name = "pydantic" +version = "2.10.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, +] + +[[package]] +name = "pydantic-core" +version = "2.27.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/bc/fed5f74b5d802cf9a03e83f60f18864e90e3aed7223adaca5ffb7a8d8d64/pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa", size = 1895938 }, + { url = "https://files.pythonhosted.org/packages/71/2a/185aff24ce844e39abb8dd680f4e959f0006944f4a8a0ea372d9f9ae2e53/pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c", size = 1815684 }, + { url = "https://files.pythonhosted.org/packages/c3/43/fafabd3d94d159d4f1ed62e383e264f146a17dd4d48453319fd782e7979e/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a", size = 1829169 }, + { url = "https://files.pythonhosted.org/packages/a2/d1/f2dfe1a2a637ce6800b799aa086d079998959f6f1215eb4497966efd2274/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5", size = 1867227 }, + { url = "https://files.pythonhosted.org/packages/7d/39/e06fcbcc1c785daa3160ccf6c1c38fea31f5754b756e34b65f74e99780b5/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c", size = 2037695 }, + { url = "https://files.pythonhosted.org/packages/7a/67/61291ee98e07f0650eb756d44998214231f50751ba7e13f4f325d95249ab/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7", size = 2741662 }, + { url = "https://files.pythonhosted.org/packages/32/90/3b15e31b88ca39e9e626630b4c4a1f5a0dfd09076366f4219429e6786076/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a", size = 1993370 }, + { url = "https://files.pythonhosted.org/packages/ff/83/c06d333ee3a67e2e13e07794995c1535565132940715931c1c43bfc85b11/pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236", size = 1996813 }, + { url = "https://files.pythonhosted.org/packages/7c/f7/89be1c8deb6e22618a74f0ca0d933fdcb8baa254753b26b25ad3acff8f74/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962", size = 2005287 }, + { url = "https://files.pythonhosted.org/packages/b7/7d/8eb3e23206c00ef7feee17b83a4ffa0a623eb1a9d382e56e4aa46fd15ff2/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9", size = 2128414 }, + { url = "https://files.pythonhosted.org/packages/4e/99/fe80f3ff8dd71a3ea15763878d464476e6cb0a2db95ff1c5c554133b6b83/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af", size = 2155301 }, + { url = "https://files.pythonhosted.org/packages/2b/a3/e50460b9a5789ca1451b70d4f52546fa9e2b420ba3bfa6100105c0559238/pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4", size = 1816685 }, + { url = "https://files.pythonhosted.org/packages/57/4c/a8838731cb0f2c2a39d3535376466de6049034d7b239c0202a64aaa05533/pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31", size = 1982876 }, + { url = "https://files.pythonhosted.org/packages/c2/89/f3450af9d09d44eea1f2c369f49e8f181d742f28220f88cc4dfaae91ea6e/pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc", size = 1893421 }, + { url = "https://files.pythonhosted.org/packages/9e/e3/71fe85af2021f3f386da42d291412e5baf6ce7716bd7101ea49c810eda90/pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7", size = 1814998 }, + { url = "https://files.pythonhosted.org/packages/a6/3c/724039e0d848fd69dbf5806894e26479577316c6f0f112bacaf67aa889ac/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15", size = 1826167 }, + { url = "https://files.pythonhosted.org/packages/2b/5b/1b29e8c1fb5f3199a9a57c1452004ff39f494bbe9bdbe9a81e18172e40d3/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306", size = 1865071 }, + { url = "https://files.pythonhosted.org/packages/89/6c/3985203863d76bb7d7266e36970d7e3b6385148c18a68cc8915fd8c84d57/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99", size = 2036244 }, + { url = "https://files.pythonhosted.org/packages/0e/41/f15316858a246b5d723f7d7f599f79e37493b2e84bfc789e58d88c209f8a/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459", size = 2737470 }, + { url = "https://files.pythonhosted.org/packages/a8/7c/b860618c25678bbd6d1d99dbdfdf0510ccb50790099b963ff78a124b754f/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048", size = 1992291 }, + { url = "https://files.pythonhosted.org/packages/bf/73/42c3742a391eccbeab39f15213ecda3104ae8682ba3c0c28069fbcb8c10d/pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d", size = 1994613 }, + { url = "https://files.pythonhosted.org/packages/94/7a/941e89096d1175d56f59340f3a8ebaf20762fef222c298ea96d36a6328c5/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b", size = 2002355 }, + { url = "https://files.pythonhosted.org/packages/6e/95/2359937a73d49e336a5a19848713555605d4d8d6940c3ec6c6c0ca4dcf25/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474", size = 2126661 }, + { url = "https://files.pythonhosted.org/packages/2b/4c/ca02b7bdb6012a1adef21a50625b14f43ed4d11f1fc237f9d7490aa5078c/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6", size = 2153261 }, + { url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361 }, + { url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484 }, + { url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102 }, + { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 }, + { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 }, + { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 }, + { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 }, + { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 }, + { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 }, + { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 }, + { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 }, + { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 }, + { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 }, + { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 }, + { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 }, + { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 }, + { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 }, + { url = "https://files.pythonhosted.org/packages/46/72/af70981a341500419e67d5cb45abe552a7c74b66326ac8877588488da1ac/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e", size = 1891159 }, + { url = "https://files.pythonhosted.org/packages/ad/3d/c5913cccdef93e0a6a95c2d057d2c2cba347815c845cda79ddd3c0f5e17d/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8", size = 1768331 }, + { url = "https://files.pythonhosted.org/packages/f6/f0/a3ae8fbee269e4934f14e2e0e00928f9346c5943174f2811193113e58252/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3", size = 1822467 }, + { url = "https://files.pythonhosted.org/packages/d7/7a/7bbf241a04e9f9ea24cd5874354a83526d639b02674648af3f350554276c/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f", size = 1979797 }, + { url = "https://files.pythonhosted.org/packages/4f/5f/4784c6107731f89e0005a92ecb8a2efeafdb55eb992b8e9d0a2be5199335/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133", size = 1987839 }, + { url = "https://files.pythonhosted.org/packages/6d/a7/61246562b651dff00de86a5f01b6e4befb518df314c54dec187a78d81c84/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc", size = 1998861 }, + { url = "https://files.pythonhosted.org/packages/86/aa/837821ecf0c022bbb74ca132e117c358321e72e7f9702d1b6a03758545e2/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50", size = 2116582 }, + { url = "https://files.pythonhosted.org/packages/81/b0/5e74656e95623cbaa0a6278d16cf15e10a51f6002e3ec126541e95c29ea3/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9", size = 2151985 }, + { url = "https://files.pythonhosted.org/packages/63/37/3e32eeb2a451fddaa3898e2163746b0cffbbdbb4740d38372db0490d67f3/pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151", size = 2004715 }, +] + +[[package]] +name = "pydantic-settings" +version = "2.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/7b/c58a586cd7d9ac66d2ee4ba60ca2d241fa837c02bca9bea80a9a8c3d22a9/pydantic_settings-2.7.1.tar.gz", hash = "sha256:10c9caad35e64bfb3c2fbf70a078c0e25cc92499782e5200747f942a065dec93", size = 79920 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/46/93416fdae86d40879714f72956ac14df9c7b76f7d41a4d68aa9f71a0028b/pydantic_settings-2.7.1-py3-none-any.whl", hash = "sha256:590be9e6e24d06db33a4262829edef682500ef008565a969c73d39d5f8bfb3fd", size = 29718 }, +] + +[[package]] +name = "pydeck" +version = "0.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/ca/40e14e196864a0f61a92abb14d09b3d3da98f94ccb03b49cf51688140dab/pydeck-0.9.1.tar.gz", hash = "sha256:f74475ae637951d63f2ee58326757f8d4f9cd9f2a457cf42950715003e2cb605", size = 3832240 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/4c/b888e6cf58bd9db9c93f40d1c6be8283ff49d88919231afe93a6bcf61626/pydeck-0.9.1-py2.py3-none-any.whl", hash = "sha256:b3f75ba0d273fc917094fa61224f3f6076ca8752b93d46faf3bcfd9f9d59b038", size = 6900403 }, +] + +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, +] + +[[package]] +name = "pymupdf" +version = "1.24.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pymupdfb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/0c/ac4d70ca95f46e3abec2ce561b5d75e79dfd4b0f930a39b3f98229cb8c5a/PyMuPDF-1.24.5.tar.gz", hash = "sha256:968d30ab20be828aab56bb04621bbb9f962c59ab5f1788236473c372fe797093", size = 22456143 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/45/f3a043b32f0cd0ea3d9569c8face1e029ec1922950ca52cbbddfc19a4122/PyMuPDF-1.24.5-cp310-none-macosx_10_9_x86_64.whl", hash = "sha256:6f8514e345702c1978c8888a5515f0b38325d688fd377e7fc1d0744b92cca934", size = 3182945 }, + { url = "https://files.pythonhosted.org/packages/61/c0/01912e3ab34096ba020f929ad6493fd870950a05e52b854d81852772ec0e/PyMuPDF-1.24.5-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:b2b089cd75b28479d999aa0d02e25c2148a3048459777457dbb9f583a6d9926e", size = 2961337 }, + { url = "https://files.pythonhosted.org/packages/cb/ea/d0019bf244d2e0953446ada817d80feb6e728b2bec8887b927ee7e601ce5/PyMuPDF-1.24.5-cp310-none-manylinux2014_aarch64.whl", hash = "sha256:ddecb4098b843bf50f86e50fe418dcc382fe536225cbf07d98249f5db45c154b", size = 3200007 }, + { url = "https://files.pythonhosted.org/packages/e6/23/c0680dc5c72f952248a5a23297e7ea8b773fd181009d48c1ed695a1185d0/PyMuPDF-1.24.5-cp310-none-manylinux2014_x86_64.whl", hash = "sha256:0741a8bae81fe72ae12984c969a1f2bd47aa97e757d3a457ee46386c70b260c3", size = 3472528 }, + { url = "https://files.pythonhosted.org/packages/18/cb/af2ef8a1019a49b63fdd468fc13dc88d30e9f2ad2dc79b74de771e224d5f/PyMuPDF-1.24.5-cp310-none-win32.whl", hash = "sha256:e1025960650ed601f2707ba5623c08d73a4cef15ebbbf65cbbbb4e0eadd71a03", size = 2685407 }, + { url = "https://files.pythonhosted.org/packages/74/65/b32c9ae88a5d9af4e7f347ccd7865c5075c24165d1a47bbdfe4c20393fad/PyMuPDF-1.24.5-cp310-none-win_amd64.whl", hash = "sha256:c2ee61ba4b6302d67c88e7e0c5b3fbea2c4c4c05f567182c465a1b9af6067e51", size = 3209615 }, + { url = "https://files.pythonhosted.org/packages/49/a8/26e757dd9a624c6cd1d144f4138ad0b10fa4b5844676943cd20dc13f7dd9/PyMuPDF-1.24.5-cp311-none-macosx_10_9_x86_64.whl", hash = "sha256:eb145b5372b6faf6a4ec86dee51c12a55bd30c0c9df04fc1146d61710726cd08", size = 3182946 }, + { url = "https://files.pythonhosted.org/packages/d0/8f/9f033131ca9993f1b0acc2ca06a13f4f0a65c3798be167d4234ae83a0b6e/PyMuPDF-1.24.5-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:131cf58e9932c84b0d367621cd580301a0d5d67590ce97a4d484b5c6c12bc8a5", size = 2961339 }, + { url = "https://files.pythonhosted.org/packages/91/f1/c1bea052eae68f4a4eecbb90ae52cd0cd9df0bddfa157ba8494063dbea31/PyMuPDF-1.24.5-cp311-none-manylinux2014_aarch64.whl", hash = "sha256:318befef651f1b98c6de4980d725970c93eddd3f183fa0bf5e47decfbed918a9", size = 3200104 }, + { url = "https://files.pythonhosted.org/packages/1b/e3/49b4ab0ddb70bf771245e1a4adc3265008d8a9fb1eeb6b423f4dd02d21f5/PyMuPDF-1.24.5-cp311-none-manylinux2014_x86_64.whl", hash = "sha256:4634f3e89aec71686b56c6db0e5932314d5c220ba452bf82b00650ff3bbed662", size = 3472389 }, + { url = "https://files.pythonhosted.org/packages/37/e8/0c01852922f2726bfcdd574cf7a8efaf7e69d450e071d1834ef4695a3fe1/PyMuPDF-1.24.5-cp311-none-win32.whl", hash = "sha256:dfe44284e1c753376d111f3edf8c11f7daca96c72fe65f97d41ed71704a640e1", size = 2685528 }, + { url = "https://files.pythonhosted.org/packages/c8/aa/0416baa202137e7f947b7fa14f34416441dce843df82eda43a557f10a1e2/PyMuPDF-1.24.5-cp311-none-win_amd64.whl", hash = "sha256:8b2b1b08ce5b36168c625ed2a3621a6fd56e77f433a68ab8cd0445b616a59ec6", size = 3209689 }, + { url = "https://files.pythonhosted.org/packages/fa/03/42e9daefb45fe100b5d9532c3f0fe954dfba1d15391582b02e6424cdda60/PyMuPDF-1.24.5-cp312-none-macosx_10_9_x86_64.whl", hash = "sha256:8d6e5e3a81979ec2cc1a1647395fbc224a57dbe91a6e65d3ab3e4644e1137b90", size = 3218789 }, + { url = "https://files.pythonhosted.org/packages/16/ba/4bc3a7c23e1092b04d3102fed26b8401afecc6c14eba127ba9c3708dbca8/PyMuPDF-1.24.5-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:65f999519b87747f2220194eb9251c3d18dc7191da56acd9b38641c8f093caaf", size = 2967881 }, + { url = "https://files.pythonhosted.org/packages/d8/3d/4a9d68e73a9cb6ac6fec9be2bb6b9e6e364f3ad2842d6720ffbc6b281c81/PyMuPDF-1.24.5-cp312-none-manylinux2014_aarch64.whl", hash = "sha256:2be7be2e8e3db25f8590a2cbf9790cf316ae8719beb5a29c37f6ebcf1f1cd781", size = 3208641 }, + { url = "https://files.pythonhosted.org/packages/fc/e4/eff5e9e987fe9c242f89dc89c10f64aa82e5b55ed132a0ab77cd13565bfe/PyMuPDF-1.24.5-cp312-none-manylinux2014_x86_64.whl", hash = "sha256:9f75be74de4852e7addd8b8a1baf49eb2fc9698fc522ce5a98fc046f2a1ae19c", size = 3479350 }, + { url = "https://files.pythonhosted.org/packages/2f/3e/d0907b7455822b31fd279254965025c353c63297dec243835d3e80b01378/PyMuPDF-1.24.5-cp312-none-win32.whl", hash = "sha256:b0d660a5e157791e0ffecffc2b98c8a038adcad57d84c068a25e4f21c4044c13", size = 2690033 }, + { url = "https://files.pythonhosted.org/packages/ef/01/48e356f470e8e6a2d77a64567cf64f49fdd82ede3050c7b7fe29ce0e9975/PyMuPDF-1.24.5-cp312-none-win_amd64.whl", hash = "sha256:13acfd2f616f0628fe0cf1461438f112a455d143924bb0fcfb305701ff9698e2", size = 3217992 }, +] + +[[package]] +name = "pymupdfb" +version = "1.24.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/0e/ece14146cd8d0397149235ded80a671a6a205b9e39fe80cc645723fe2832/PyMuPDFb-1.24.3.tar.gz", hash = "sha256:7cc5da3031d160e0f01dbb88567ddca70adc82f062a3a5b4e2dd2a57646f442c", size = 37496 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/52/d7a3351eebb23b94f3caac648290c69363d5d5e28e5c49e0b7997925d8b6/PyMuPDFb-1.24.3-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:d2ccca660042896d4af479f979ec10674c5a0b3cd2d9ecb0011f08dc82380cce", size = 15296057 }, + { url = "https://files.pythonhosted.org/packages/7e/4a/27e4e2ce8f5d0ed1d1b2a1f7807f6158db1e8e547a7bf76ac462a800a4b4/PyMuPDFb-1.24.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ad51d21086a16199684a3eebcb47d9c8460fc27e7bebae77f5fe64e8c34ebf34", size = 14911671 }, + { url = "https://files.pythonhosted.org/packages/6c/7a/3b8347945a75e0b12817484080dd60913bc006d0cba3f11b7a3578007bda/PyMuPDFb-1.24.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e7aab000d707c40e3254cd60152897b90952ed9a3567584d70974292f4912ce", size = 15514309 }, + { url = "https://files.pythonhosted.org/packages/ed/77/8a25d8e9189f2c7f9f1c20c77dc74927630cb46e59d999056039142cce50/PyMuPDFb-1.24.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f39588fd2b7a63e2456df42cd8925c316202e0eb77d115d9c01ba032b2c9086f", size = 15750766 }, + { url = "https://files.pythonhosted.org/packages/0a/d6/c91d17e247e04962f4db9d414a23d8a3a16fe4a4ed104202117f16d4d011/PyMuPDFb-1.24.3-py3-none-win32.whl", hash = "sha256:0d606a10cb828cefc9f864bf67bc9d46e8007af55e643f022b59d378af4151a8", size = 11007908 }, + { url = "https://files.pythonhosted.org/packages/a2/e5/d2cba4c62c09ed54ba79d4d586517268a6a66b464cc9291a7647352e8124/PyMuPDFb-1.24.3-py3-none-win_amd64.whl", hash = "sha256:e88289bd4b4afe5966a028774b302f37d4b51dad5c5e6720dd04524910db6c6e", size = 12443485 }, +] + +[[package]] +name = "pypdf2" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/bb/18dc3062d37db6c491392007dfd1a7f524bb95886eb956569ac38a23a784/PyPDF2-3.0.1.tar.gz", hash = "sha256:a74408f69ba6271f71b9352ef4ed03dc53a31aa404d29b5d31f53bfecfee1440", size = 227419 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/5e/c86a5643653825d3c913719e788e41386bee415c2b87b4f955432f2de6b2/pypdf2-3.0.1-py3-none-any.whl", hash = "sha256:d16e4205cfee272fbdc0568b68d82be796540b1537508cef59388f839c191928", size = 232572 }, +] + +[[package]] +name = "pyreadline3" +version = "3.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/49/4cea918a08f02817aabae639e3d0ac046fef9f9180518a3ad394e22da148/pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7", size = 99839 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178 }, +] + +[[package]] +name = "pytest" +version = "8.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, +] + +[[package]] +name = "pytest-sugar" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "pytest" }, + { name = "termcolor" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/ac/5754f5edd6d508bc6493bc37d74b928f102a5fff82d9a80347e180998f08/pytest-sugar-1.0.0.tar.gz", hash = "sha256:6422e83258f5b0c04ce7c632176c7732cab5fdb909cb39cca5c9139f81276c0a", size = 14992 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/fb/889f1b69da2f13691de09a111c16c4766a433382d44aa0ecf221deded44a/pytest_sugar-1.0.0-py3-none-any.whl", hash = "sha256:70ebcd8fc5795dc457ff8b69d266a4e2e8a74ae0c3edc749381c64b5246c8dfd", size = 10171 }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, +] + +[[package]] +name = "python-dotenv" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, +] + +[[package]] +name = "pytz" +version = "2025.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/57/df1c9157c8d5a05117e455d66fd7cf6dbc46974f832b1058ed4856785d8a/pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e", size = 319617 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/38/ac33370d784287baa1c3d538978b5e2ea064d4c1b93ffbd12826c190dd10/pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57", size = 507930 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, +] + +[[package]] +name = "questionary" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "prompt-toolkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/b8/d16eb579277f3de9e56e5ad25280fab52fc5774117fb70362e8c2e016559/questionary-2.1.0.tar.gz", hash = "sha256:6302cdd645b19667d8f6e6634774e9538bfcd1aad9be287e743d96cacaf95587", size = 26775 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/3f/11dd4cd4f39e05128bfd20138faea57bec56f9ffba6185d276e3107ba5b2/questionary-2.1.0-py3-none-any.whl", hash = "sha256:44174d237b68bc828e4878c763a9ad6790ee61990e0ae72927694ead57bab8ec", size = 36747 }, +] + +[[package]] +name = "referencing" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "rich" +version = "13.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, +] + +[[package]] +name = "rich-click" +version = "1.8.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ea/e3/ff1c715b673ec9e01f4482d8d0edfd9adf891f3630d83e695b38337a3889/rich_click-1.8.6.tar.gz", hash = "sha256:8a2448fd80e3d4e16fcb3815bfbc19be9bae75c9bb6aedf637901e45f3555752", size = 38247 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/09/c20b04b6c9cf273995753f226ca51656e00f8a37f1e723f8c713b93b2ad4/rich_click-1.8.6-py3-none-any.whl", hash = "sha256:55fb571bad7d3d69ac43ca45f05b44616fd019616161b1815ff053567b9a8e22", size = 35076 }, +] + +[[package]] +name = "rpds-py" +version = "0.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/0e/4c797078d00dbf1f63af96e4b3beffb67f71230f58442272b4b1962a61c8/rpds_py-0.23.0.tar.gz", hash = "sha256:ffac3b13182dc1bf648cde2982148dc9caf60f3eedec7ae639e05636389ebf5d", size = 26808 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/39/c2b2337f0dd0494a7e66eb3f114dde2a08c613b9374f52fdec8c38890def/rpds_py-0.23.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:1b36e993b95f0744a94a5add7624cfaf77b91805819c1a960026bc452f95841e", size = 372147 }, + { url = "https://files.pythonhosted.org/packages/1c/19/cd8c3bc46b5151db48b9f411cc7cd957766220f93d0edcb8cb81cdc8b192/rpds_py-0.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:72a0dd4d599fadaf519d4e4b8092e5d7940057c61e70f9f06c1d004a47895204", size = 356821 }, + { url = "https://files.pythonhosted.org/packages/c2/01/da56c2a7ca4dfdb8809d9a6a8a28c3addbff67801f01d2f9fdc65accb77b/rpds_py-0.23.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bba83d703c6728a3a2676a14a9649d7cc87b9e4654293f13f8d4b4d7007d6383", size = 386041 }, + { url = "https://files.pythonhosted.org/packages/b6/b4/113a77a972f92571b12bb596aea4c2d2b80b05ef334a5c330816d07c536c/rpds_py-0.23.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1191bf5975a0b001c161a62d5833a6b2f838b10ff19e203910dd6210e88d89f5", size = 392084 }, + { url = "https://files.pythonhosted.org/packages/11/27/72ba0416f0d96f60c9d8ca5ba5d4fdd8b934450005a40249c7ef34e54a8f/rpds_py-0.23.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3154e132e685f907813ace8701721ad4420244f6e07afc2a61763894e8a22961", size = 445971 }, + { url = "https://files.pythonhosted.org/packages/db/e6/c6d46137847b6add2e93ce2190f7bf190b72e7e19c78e96192e8c66d84f7/rpds_py-0.23.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62d8fe953110a98a118cacdc1ca79fe344a946c72a2d19fa7d17d0b2ace58f3d", size = 448102 }, + { url = "https://files.pythonhosted.org/packages/1d/f9/eca747830f4e687afbf78c573554aa46775f0c89d81f584e8ec49c80149a/rpds_py-0.23.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e27dfcea222c81cd8bece98a73ebb8ca69870de01dc27002d433ad06e55dd8b", size = 386631 }, + { url = "https://files.pythonhosted.org/packages/38/e0/d6db6913ed3713c3bfd7e4661cb15663cb7264c981f7b0c9fa2d1e766aaf/rpds_py-0.23.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7cca21adfefe5a2237f1e64d769c1ed7ccdc2515d376d1774e7fbe918e03cd8c", size = 415922 }, + { url = "https://files.pythonhosted.org/packages/cf/df/241701a9f48257e285b8b2dbe9cf960a2b43620e7c23e2d3d6edec88b57f/rpds_py-0.23.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8c708f5c2d604e0acc9489df3ea879f4fc75030dfa590668fd959fda34fcc0b8", size = 558329 }, + { url = "https://files.pythonhosted.org/packages/9d/c0/2ba807c157b776247605b106cf4c9511ced66afc14aa6cdb0fa89eefd69a/rpds_py-0.23.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c23cbff21154951731866358e983d01d536a2c0f60f2765be85f00682eae60d9", size = 585099 }, + { url = "https://files.pythonhosted.org/packages/13/9c/c89ab214ce56a696bd1238c1f1f763de1921eff7ccf4f1ee8f1126e71b07/rpds_py-0.23.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:16826a5346e293bedf0acd5c2f4c8e05415b1970aa3cc448eea19f02724dd453", size = 553833 }, + { url = "https://files.pythonhosted.org/packages/73/33/807356b2f364cf5f0a0abe0fa103a8653d810100a346a7487c8185551fbf/rpds_py-0.23.0-cp310-cp310-win32.whl", hash = "sha256:1e0fb88357f59c70b8595bc8e5887be35636e646a9ab519c1876063159812cf6", size = 220694 }, + { url = "https://files.pythonhosted.org/packages/a1/fe/a0b8e4545125489821ca0b48368dc3815967f0e0f7dd7e8128a06fd078f8/rpds_py-0.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:c79544d0be2c7c3891fe448bc006666410bc219fdf29bf35990f0ea88ff72b64", size = 232519 }, + { url = "https://files.pythonhosted.org/packages/52/15/23b410a7c69910830334be21c243aecc1e7108fb8e18cbe6d0444c12a3a7/rpds_py-0.23.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:827b334702a04df2e1b7fe85ed3784512f6fd3d3a40259180db0c8fdeb20b37f", size = 372317 }, + { url = "https://files.pythonhosted.org/packages/72/8a/d4fcda85b8504b17c5fe0536aef69ba54c72cbf8bfdd4bf204ce7fd7077d/rpds_py-0.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0e1ece346395e127a8024e5c13d304bdd7dbd094e05329a2f4f27ea1fbe14aa3", size = 357029 }, + { url = "https://files.pythonhosted.org/packages/31/4b/73a1b121b204f78ccd8977ea5fa29c47805368e76c5f298f431cefa99701/rpds_py-0.23.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adc0b2e71e62fde524389634df4b53f4d16d5f3830ab35c1e511d50b75674f6", size = 385916 }, + { url = "https://files.pythonhosted.org/packages/a7/ac/1a36dfce2fcf1c48f5683de9cc544c8dbb0b234cede287d9b4b529b83282/rpds_py-0.23.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1eb4757f9c9f96e26a420db97c3ecaa97568961ce718f1f89e03ce1f59ec12e", size = 392393 }, + { url = "https://files.pythonhosted.org/packages/50/6d/53abb5a78113e6ab7848caf3efaad8331ff5f6b69b921e1697a03e1becf5/rpds_py-0.23.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e17402e8f3b49a7ec22e7ef7bbbe0ac0797fcbf0f1ba844811668ef24b37fc9d", size = 446085 }, + { url = "https://files.pythonhosted.org/packages/6a/bc/7ea54464f685fb7d1c5ff4c580381deef946fdd693f0817cd4825bdd88e5/rpds_py-0.23.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8212c5d25514386a14a032fde7f7f0383a88355f93a1d0fde453f38ebdc43a1b", size = 447603 }, + { url = "https://files.pythonhosted.org/packages/42/12/56bb6ce379b3221df6a9935709838dedc6cf37b89526b3a09a257b514be2/rpds_py-0.23.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5211b646a0beb1f8f4b1cde8c7c073f9d6ca3439d5a93ea0874c8ece6cab66a9", size = 386630 }, + { url = "https://files.pythonhosted.org/packages/d8/f2/5428a298394ca06fd80a812e56b33a00d439d9df1481316b8b9ec6143f95/rpds_py-0.23.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:83f71359d81cfb3bd39522045d08a7031036fb0b1b0a43a066c094cc52a9fd00", size = 416041 }, + { url = "https://files.pythonhosted.org/packages/02/f3/66bc3656fcff45fa0954113f409d1f6cdd9c1cefcc1c49d5f9c45526acaf/rpds_py-0.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9e66aaa24e0dc3cfaf63a8fc2810ae296792c18fb4cfb99868f52e7c598911b6", size = 557641 }, + { url = "https://files.pythonhosted.org/packages/e7/0c/3fad69ea0c64e496cac0b6c85ca3e12edadbaa65938130fc714227d02634/rpds_py-0.23.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:35336790b4d70c31a59c922d7d603010fe13c5ff56a1dce14849b6bb6a2ad4b9", size = 584504 }, + { url = "https://files.pythonhosted.org/packages/2a/e3/f42acd14d5d655860ddf9dee3c5c03122a95235bd63885f7b5462dcaef16/rpds_py-0.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:377ba75ebce48d5df69b0ab2e3333cd86f6acfee8cf0a2c286af4e32e4a8b499", size = 552964 }, + { url = "https://files.pythonhosted.org/packages/f5/0d/90d93159bb22f1b347ee4aab3797c52c0dbb18249e80486a98703778bc2e/rpds_py-0.23.0-cp311-cp311-win32.whl", hash = "sha256:784a79474675ee12cab90241f3df328129e15443acfea618df069a7d67d12abb", size = 220878 }, + { url = "https://files.pythonhosted.org/packages/2f/20/9afb6a947956e06762a3f25d564b1058e81e830dad80609710a80e9a057d/rpds_py-0.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:f1023b1de400ef9d3d9f8f9e88f3f5d8c66c26e48c3f83cffe83bd423def8d81", size = 232947 }, + { url = "https://files.pythonhosted.org/packages/57/cd/c57b77ae0c8514b316c65668e3d016857b738738496a77faf64346c9f943/rpds_py-0.23.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:d1f3baf652aeb91775eb3343535890156b07e0cbb2a7b72651f4bbaf7323d40f", size = 364451 }, + { url = "https://files.pythonhosted.org/packages/8f/d7/76cdd973a44be9692db2ae1f38264e565af99c2b9aabbcabb87670618322/rpds_py-0.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6593dc9b225f8fc900df43c40625c998b8fa99ba78ec69bcd073fe3fb1018a5d", size = 349905 }, + { url = "https://files.pythonhosted.org/packages/03/7a/412a30ecfb8e24a00ad3a7583664d2aa0e3729d3459237241e39b85b926d/rpds_py-0.23.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75d5a2c5629e3582aa73c3a11ac0a3dd454e86cc70188a9b6e2ed51889c331dd", size = 388801 }, + { url = "https://files.pythonhosted.org/packages/c3/68/995cc01425aaa650fda11367457647463d7d89b0f14e9d90ba23d9a168af/rpds_py-0.23.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:64ba22924340d7e200b48befcc75ff2379301902381ca4ebbfec81d80c5216b5", size = 397582 }, + { url = "https://files.pythonhosted.org/packages/18/92/349638b77ace990f62e1241fabda9f2b807f33370cef8218a6e7a425c709/rpds_py-0.23.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04d7fc114ca57d25f0d8c324d2d0ddd675df92b2f7da8284f806711c25fe00f7", size = 448746 }, + { url = "https://files.pythonhosted.org/packages/92/87/79f50a864a2e0a7f1bcf5460d4747a81e75e37d458b430947e726432e669/rpds_py-0.23.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ff50d7a5b206af7ac8342255ae3ab6c6c86d86520f4413bf9d2561bf4f1ffa1", size = 442565 }, + { url = "https://files.pythonhosted.org/packages/6a/5f/87c3ddb265f6380193f167bb811a7ae0240812205bbb64c5ed53430751ff/rpds_py-0.23.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b147c0d49de69dac573c8e05a5f7edf18a83136bf8c98e2cd3e87dafee184e5", size = 390652 }, + { url = "https://files.pythonhosted.org/packages/24/cd/5758fe38126dd7cb8a9cca851080253f74e0ca55b3cacab67365e283836c/rpds_py-0.23.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5bc79d528e65c877a5e254ddad394d51797bc6bba44c9aa436f61b94448d5f87", size = 421371 }, + { url = "https://files.pythonhosted.org/packages/42/98/f3b161e66a381e39c9bf153562f90d15b01272ac943374262cda2ce54401/rpds_py-0.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ce1a2fe8eea2e956a11112ba426b9be79b2da65e27a533cf152ba8e9882bf9be", size = 562353 }, + { url = "https://files.pythonhosted.org/packages/70/8e/a9bbfb09eb849ae282a3970927155fc8608be536b53bc0799206c757b807/rpds_py-0.23.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e2c26f1e0ebbe85dc275816cd53fcbb225aaf7923a4d48b7cdf8b8eb6291e5ae", size = 588318 }, + { url = "https://files.pythonhosted.org/packages/12/1d/c404fd00d37e44299f2e0c19ad9fc1f5c4aed489fc6a6146cdca889b818b/rpds_py-0.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6893a88925972635c843eb02a113d7aabacd386c05d54f2fda29125befbc1b05", size = 557251 }, + { url = "https://files.pythonhosted.org/packages/bb/0f/7c4bfdc764fc3042c65b90a9c3e251124b43ccf3c90fb36cddc29846fe86/rpds_py-0.23.0-cp312-cp312-win32.whl", hash = "sha256:06962dc9462fe97d0355e01525ebafcd317316e80e335272751a1857b7bdec97", size = 222227 }, + { url = "https://files.pythonhosted.org/packages/54/2d/8fcb5dd574687ed729b28095e96bb54534368d949294eae11f0c6fa3a6c6/rpds_py-0.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:04882cc4adbdc2778dd49f5ed71b1d9ab43349c45cde7e461456d0432d7d323e", size = 237508 }, + { url = "https://files.pythonhosted.org/packages/8e/a9/026caac2fbf61bae16353434299e1b90465e5c9c1b672573e98b5381ba74/rpds_py-0.23.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b233a2bdb15dbb4c05b0c79c94d2367a05d0c54351b76c74fdc81aae023a2df8", size = 373410 }, + { url = "https://files.pythonhosted.org/packages/a4/0a/c31de6f41377b0779958ad73df175d837ea0add4cc65874cfae65a74edf5/rpds_py-0.23.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d2e0cace96976f4e86fc3c51cb3fba24225976e26341e958be42f3d8d0a634ee", size = 358362 }, + { url = "https://files.pythonhosted.org/packages/ee/71/698b9bc8b372c7681c7361dc537d8fd0331d4fee464993845cc6704be5c8/rpds_py-0.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:210aa7c699cc61320630c4be33348d9bfef4785fabd6f33ea6be711d4eb45f1f", size = 387113 }, + { url = "https://files.pythonhosted.org/packages/1a/08/7ae0cf8f3920445378f244f8afe01e9292268d4006aec76415edd334f4d6/rpds_py-0.23.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cd550ee493adab33e95ce00cb42529b0435c916ed949d298887ee9acdcd3f2f", size = 393436 }, + { url = "https://files.pythonhosted.org/packages/a5/11/3c5a90d08a9250397f9fe68bd82c0eaf49f1fb6e732b685edc73cc72a4f0/rpds_py-0.23.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:174602fe067a5b622ce47a5b09022e0128c526a308354abd9cc4bf0391f3cfd2", size = 446291 }, + { url = "https://files.pythonhosted.org/packages/72/e3/2e416b5654d1c61cdce254252881ea25e77a2f2635dc28cdb3a3edf21df9/rpds_py-0.23.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8b7b4e5cc5a981a147e1602cf4bd517e57617f9a4c7e96a22a27e4d18de2523", size = 447989 }, + { url = "https://files.pythonhosted.org/packages/51/c7/3da1d282fd6b4ac285a7940d92d8cf55b2be8ffb86c3a60223ad57949301/rpds_py-0.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d67acbcf2cb11acd44da7d41a0495b7799a32fb7ec9a6bc0b14d8552e00fb", size = 388117 }, + { url = "https://files.pythonhosted.org/packages/b9/88/e6feed21bd9e4dd4f219610795787b2dc34cc7d3e68ddc199d2ee8c04991/rpds_py-0.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f482453aeebdae7774781e8c9b1884e0df0bdb1c61f330f95c63a401dfc2fc31", size = 418237 }, + { url = "https://files.pythonhosted.org/packages/42/1d/3d09dfb0b6086d0e802d7ef9c7ccf9958aff83afc178816cd2332e51dd2d/rpds_py-0.23.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:eb841a8e1c2615dfc721d3c28fe81e6300e819a01d3305ecd7f75c7d58c31b2b", size = 559116 }, + { url = "https://files.pythonhosted.org/packages/66/8e/92a7236a829910374322b9cfd9ae0508785f960d223f2932da322dffd9fa/rpds_py-0.23.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:41f6bb731bfcbd886bd6399717971dd881d759ea831b9f513bc57a10f52c7d53", size = 584690 }, + { url = "https://files.pythonhosted.org/packages/4a/5d/c5f9ca93b481a084a7e7b92d947794f4b2dba2b41c875ec1c9e2554682d3/rpds_py-0.23.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:a49aeb989ee5e057137910059610bfa8f571a4af674404ce05c59862bbeeecbe", size = 555151 }, + { url = "https://files.pythonhosted.org/packages/90/ab/4278bb3f4908436b6f752daf3d874f63f681ebc965d915dc1c2ea2e842d1/rpds_py-0.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:670c29a74f8e632aa58b48425b12d026703af1ea5e3b131adbb2601c7ae03108", size = 233854 }, +] + +[[package]] +name = "ruff" +version = "0.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/e1/e265aba384343dd8ddd3083f5e33536cd17e1566c41453a5517b5dd443be/ruff-0.9.6.tar.gz", hash = "sha256:81761592f72b620ec8fa1068a6fd00e98a5ebee342a3642efd84454f3031dca9", size = 3639454 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/e3/3d2c022e687e18cf5d93d6bfa2722d46afc64eaa438c7fbbdd603b3597be/ruff-0.9.6-py3-none-linux_armv6l.whl", hash = "sha256:2f218f356dd2d995839f1941322ff021c72a492c470f0b26a34f844c29cdf5ba", size = 11714128 }, + { url = "https://files.pythonhosted.org/packages/e1/22/aff073b70f95c052e5c58153cba735748c9e70107a77d03420d7850710a0/ruff-0.9.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b908ff4df65dad7b251c9968a2e4560836d8f5487c2f0cc238321ed951ea0504", size = 11682539 }, + { url = "https://files.pythonhosted.org/packages/75/a7/f5b7390afd98a7918582a3d256cd3e78ba0a26165a467c1820084587cbf9/ruff-0.9.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b109c0ad2ececf42e75fa99dc4043ff72a357436bb171900714a9ea581ddef83", size = 11132512 }, + { url = "https://files.pythonhosted.org/packages/a6/e3/45de13ef65047fea2e33f7e573d848206e15c715e5cd56095589a7733d04/ruff-0.9.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1de4367cca3dac99bcbd15c161404e849bb0bfd543664db39232648dc00112dc", size = 11929275 }, + { url = "https://files.pythonhosted.org/packages/7d/f2/23d04cd6c43b2e641ab961ade8d0b5edb212ecebd112506188c91f2a6e6c/ruff-0.9.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac3ee4d7c2c92ddfdaedf0bf31b2b176fa7aa8950efc454628d477394d35638b", size = 11466502 }, + { url = "https://files.pythonhosted.org/packages/b5/6f/3a8cf166f2d7f1627dd2201e6cbc4cb81f8b7d58099348f0c1ff7b733792/ruff-0.9.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5dc1edd1775270e6aa2386119aea692039781429f0be1e0949ea5884e011aa8e", size = 12676364 }, + { url = "https://files.pythonhosted.org/packages/f5/c4/db52e2189983c70114ff2b7e3997e48c8318af44fe83e1ce9517570a50c6/ruff-0.9.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4a091729086dffa4bd070aa5dab7e39cc6b9d62eb2bef8f3d91172d30d599666", size = 13335518 }, + { url = "https://files.pythonhosted.org/packages/66/44/545f8a4d136830f08f4d24324e7db957c5374bf3a3f7a6c0bc7be4623a37/ruff-0.9.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1bbc6808bf7b15796cef0815e1dfb796fbd383e7dbd4334709642649625e7c5", size = 12823287 }, + { url = "https://files.pythonhosted.org/packages/c5/26/8208ef9ee7431032c143649a9967c3ae1aae4257d95e6f8519f07309aa66/ruff-0.9.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:589d1d9f25b5754ff230dce914a174a7c951a85a4e9270613a2b74231fdac2f5", size = 14592374 }, + { url = "https://files.pythonhosted.org/packages/31/70/e917781e55ff39c5b5208bda384fd397ffd76605e68544d71a7e40944945/ruff-0.9.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc61dd5131742e21103fbbdcad683a8813be0e3c204472d520d9a5021ca8b217", size = 12500173 }, + { url = "https://files.pythonhosted.org/packages/84/f5/e4ddee07660f5a9622a9c2b639afd8f3104988dc4f6ba0b73ffacffa9a8c/ruff-0.9.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5e2d9126161d0357e5c8f30b0bd6168d2c3872372f14481136d13de9937f79b6", size = 11906555 }, + { url = "https://files.pythonhosted.org/packages/f1/2b/6ff2fe383667075eef8656b9892e73dd9b119b5e3add51298628b87f6429/ruff-0.9.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:68660eab1a8e65babb5229a1f97b46e3120923757a68b5413d8561f8a85d4897", size = 11538958 }, + { url = "https://files.pythonhosted.org/packages/3c/db/98e59e90de45d1eb46649151c10a062d5707b5b7f76f64eb1e29edf6ebb1/ruff-0.9.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c4cae6c4cc7b9b4017c71114115db0445b00a16de3bcde0946273e8392856f08", size = 12117247 }, + { url = "https://files.pythonhosted.org/packages/ec/bc/54e38f6d219013a9204a5a2015c09e7a8c36cedcd50a4b01ac69a550b9d9/ruff-0.9.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:19f505b643228b417c1111a2a536424ddde0db4ef9023b9e04a46ed8a1cb4656", size = 12554647 }, + { url = "https://files.pythonhosted.org/packages/a5/7d/7b461ab0e2404293c0627125bb70ac642c2e8d55bf590f6fce85f508f1b2/ruff-0.9.6-py3-none-win32.whl", hash = "sha256:194d8402bceef1b31164909540a597e0d913c0e4952015a5b40e28c146121b5d", size = 9949214 }, + { url = "https://files.pythonhosted.org/packages/ee/30/c3cee10f915ed75a5c29c1e57311282d1a15855551a64795c1b2bbe5cf37/ruff-0.9.6-py3-none-win_amd64.whl", hash = "sha256:03482d5c09d90d4ee3f40d97578423698ad895c87314c4de39ed2af945633caa", size = 10999914 }, + { url = "https://files.pythonhosted.org/packages/e8/a8/d71f44b93e3aa86ae232af1f2126ca7b95c0f515ec135462b3e1f351441c/ruff-0.9.6-py3-none-win_arm64.whl", hash = "sha256:0e2bb706a2be7ddfea4a4af918562fdc1bcb16df255e5fa595bbd800ce322a5a", size = 10177499 }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] + +[[package]] +name = "smmap" +version = "5.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303 }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + +[[package]] +name = "socksio" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/5c/48a7d9495be3d1c651198fd99dbb6ce190e2274d0f28b9051307bdec6b85/socksio-1.0.0.tar.gz", hash = "sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac", size = 19055 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/c3/6eeb6034408dac0fa653d126c9204ade96b819c936e136c5e8a6897eee9c/socksio-1.0.0-py3-none-any.whl", hash = "sha256:95dc1f15f9b34e8d7b16f06d74b8ccf48f609af32ab33c608d08761c5dcbb1f3", size = 12763 }, +] + +[[package]] +name = "streamlit" +version = "1.42.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "altair" }, + { name = "blinker" }, + { name = "cachetools" }, + { name = "click" }, + { name = "gitpython" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "pillow" }, + { name = "protobuf" }, + { name = "pyarrow" }, + { name = "pydeck" }, + { name = "requests" }, + { name = "rich" }, + { name = "tenacity" }, + { name = "toml" }, + { name = "tornado" }, + { name = "typing-extensions" }, + { name = "watchdog", marker = "sys_platform != 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/58/b1875ccb6901cec7a721059bfd9122107d29e0b0ccfdcea9d353af6b5c52/streamlit-1.42.2.tar.gz", hash = "sha256:62026dbdcb482790933f658b096d7dd58fa70da89c1f06fbc3658b91dcd4dab2", size = 9169615 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/08/c962e38f4450dafb98af49f8988a736dd67ae9cc26a5704c43269f506bc8/streamlit-1.42.2-py2.py3-none-any.whl", hash = "sha256:e2516c7fcd17a11a85cc1999fae58ace0a6458e2b4c1a411ed3d75b1aee2eb93", size = 9559800 }, +] + +[[package]] +name = "streamlit-pdf-viewer" +version = "0.0.21" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "streamlit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/a9/68730a2e0efe3695d0bcece7f9ea2370a8f50c07d45637b040e103e95728/streamlit_pdf_viewer-0.0.21.tar.gz", hash = "sha256:53edb2b29a065fdbac1b2a5ea2fe3dca9b46f4e42d531739f04296f8729f47e6", size = 2537459 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/ea/50686908f89695b55960f8c178372815206170c76d577c6284e9ffee429d/streamlit_pdf_viewer-0.0.21-py3-none-any.whl", hash = "sha256:e710a11ed9e3c192d2a496b5ff325b9c645146a45e37b3dd48f96398dae78f1a", size = 2573920 }, +] + +[[package]] +name = "sympy" +version = "1.13.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/11/8a/5a7fd6284fa8caac23a26c9ddf9c30485a48169344b4bd3b0f02fef1890f/sympy-1.13.3.tar.gz", hash = "sha256:b27fd2c6530e0ab39e275fc9b683895367e51d5da91baa8d3d64db2565fec4d9", size = 7533196 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/ff/c87e0622b1dadea79d2fb0b25ade9ed98954c9033722eb707053d310d4f3/sympy-1.13.3-py3-none-any.whl", hash = "sha256:54612cf55a62755ee71824ce692986f23c88ffa77207b30c1368eda4a7060f73", size = 6189483 }, +] + +[[package]] +name = "tenacity" +version = "9.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/94/91fccdb4b8110642462e653d5dcb27e7b674742ad68efd146367da7bdb10/tenacity-9.0.0.tar.gz", hash = "sha256:807f37ca97d62aa361264d497b0e31e92b8027044942bfa756160d908320d73b", size = 47421 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/cb/b86984bed139586d01532a587464b5805f12e397594f19f931c4c2fbfa61/tenacity-9.0.0-py3-none-any.whl", hash = "sha256:93de0c98785b27fcf659856aa9f54bfbd399e29969b0621bc7f762bd441b4539", size = 28169 }, +] + +[[package]] +name = "termcolor" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/72/88311445fd44c455c7d553e61f95412cf89054308a1aa2434ab835075fc5/termcolor-2.5.0.tar.gz", hash = "sha256:998d8d27da6d48442e8e1f016119076b690d962507531df4890fcd2db2ef8a6f", size = 13057 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/be/df630c387a0a054815d60be6a97eb4e8f17385d5d6fe660e1c02750062b4/termcolor-2.5.0-py3-none-any.whl", hash = "sha256:37b17b5fc1e604945c2642c872a3764b5d547a48009871aea3edd3afa180afb8", size = 7755 }, +] + +[[package]] +name = "toml" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588 }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, +] + +[[package]] +name = "tomlkit" +version = "0.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/09/a439bec5888f00a54b8b9f05fa94d7f901d6735ef4e55dcec9bc37b5d8fa/tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79", size = 192885 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/b6/a447b5e4ec71e13871be01ba81f5dfc9d0af7e473da256ff46bc0e24026f/tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde", size = 37955 }, +] + +[[package]] +name = "toposort" +version = "1.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/19/8e955d90985ecbd3b9adb2a759753a6840da2dff3c569d412b2c9217678b/toposort-1.10.tar.gz", hash = "sha256:bfbb479c53d0a696ea7402601f4e693c97b0367837c8898bc6471adfca37a6bd", size = 11132 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/17/57b444fd314d5e1593350b9a31d000e7411ba8e17ce12dc7ad54ca76b810/toposort-1.10-py3-none-any.whl", hash = "sha256:cbdbc0d0bee4d2695ab2ceec97fe0679e9c10eab4b2a87a9372b929e70563a87", size = 8500 }, +] + +[[package]] +name = "tornado" +version = "6.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/59/45/a0daf161f7d6f36c3ea5fc0c2de619746cc3dd4c76402e9db545bd920f63/tornado-6.4.2.tar.gz", hash = "sha256:92bad5b4746e9879fd7bf1eb21dce4e3fc5128d71601f80005afa39237ad620b", size = 501135 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/7e/71f604d8cea1b58f82ba3590290b66da1e72d840aeb37e0d5f7291bd30db/tornado-6.4.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e828cce1123e9e44ae2a50a9de3055497ab1d0aeb440c5ac23064d9e44880da1", size = 436299 }, + { url = "https://files.pythonhosted.org/packages/96/44/87543a3b99016d0bf54fdaab30d24bf0af2e848f1d13d34a3a5380aabe16/tornado-6.4.2-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:072ce12ada169c5b00b7d92a99ba089447ccc993ea2143c9ede887e0937aa803", size = 434253 }, + { url = "https://files.pythonhosted.org/packages/cb/fb/fdf679b4ce51bcb7210801ef4f11fdac96e9885daa402861751353beea6e/tornado-6.4.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a017d239bd1bb0919f72af256a970624241f070496635784d9bf0db640d3fec", size = 437602 }, + { url = "https://files.pythonhosted.org/packages/4f/3b/e31aeffffc22b475a64dbeb273026a21b5b566f74dee48742817626c47dc/tornado-6.4.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c36e62ce8f63409301537222faffcef7dfc5284f27eec227389f2ad11b09d946", size = 436972 }, + { url = "https://files.pythonhosted.org/packages/22/55/b78a464de78051a30599ceb6983b01d8f732e6f69bf37b4ed07f642ac0fc/tornado-6.4.2-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca9eb02196e789c9cb5c3c7c0f04fb447dc2adffd95265b2c7223a8a615ccbf", size = 437173 }, + { url = "https://files.pythonhosted.org/packages/79/5e/be4fb0d1684eb822c9a62fb18a3e44a06188f78aa466b2ad991d2ee31104/tornado-6.4.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:304463bd0772442ff4d0f5149c6f1c2135a1fae045adf070821c6cdc76980634", size = 437892 }, + { url = "https://files.pythonhosted.org/packages/f5/33/4f91fdd94ea36e1d796147003b490fe60a0215ac5737b6f9c65e160d4fe0/tornado-6.4.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:c82c46813ba483a385ab2a99caeaedf92585a1f90defb5693351fa7e4ea0bf73", size = 437334 }, + { url = "https://files.pythonhosted.org/packages/2b/ae/c1b22d4524b0e10da2f29a176fb2890386f7bd1f63aacf186444873a88a0/tornado-6.4.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:932d195ca9015956fa502c6b56af9eb06106140d844a335590c1ec7f5277d10c", size = 437261 }, + { url = "https://files.pythonhosted.org/packages/b5/25/36dbd49ab6d179bcfc4c6c093a51795a4f3bed380543a8242ac3517a1751/tornado-6.4.2-cp38-abi3-win32.whl", hash = "sha256:2876cef82e6c5978fde1e0d5b1f919d756968d5b4282418f3146b79b58556482", size = 438463 }, + { url = "https://files.pythonhosted.org/packages/61/cc/58b1adeb1bb46228442081e746fcdbc4540905c87e8add7c277540934edb/tornado-6.4.2-cp38-abi3-win_amd64.whl", hash = "sha256:908b71bf3ff37d81073356a5fadcc660eb10c1476ee6e2725588626ce7e5ca38", size = 438907 }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, +] + +[[package]] +name = "typer" +version = "0.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/dca7b219718afd37a0068f4f2530a727c2b74a8b6e8e0c0080a4c0de4fcd/typer-0.15.1.tar.gz", hash = "sha256:a0588c0a7fa68a1978a069818657778f86abe6ff5ea6abf472f940a08bfe4f0a", size = 99789 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/cc/0a838ba5ca64dc832aa43f727bd586309846b0ffb2ce52422543e6075e8a/typer-0.15.1-py3-none-any.whl", hash = "sha256:7994fb7b8155b64d3402518560648446072864beefd44aa2dc36972a5972e847", size = 44908 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "typos" +version = "1.29.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/13/875246cea9b9ce686b6608ac44ec9cf4905ce0e109555d68334e98be197a/typos-1.29.9.tar.gz", hash = "sha256:481480d7c33a6a426c8728b2df65d9188f0bc855ea8390f4a4f44e713040cbc8", size = 1492087 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/52/273d3335ef1499d838fd363e107542a2e1fdc657655da20e919963a39001/typos-1.29.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4547ff98a61f71f75a721b0c401eef220c456e937d77cae141aabf97cdee155d", size = 3125093 }, + { url = "https://files.pythonhosted.org/packages/4b/46/ce73761b84bb2081b923c9b96d01e74062c700a3c3d852a0476869965edb/typos-1.29.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fb74645a60eb76d4a147de36968d5db51ee6bd35e72a9210e462c96bbe30e7e9", size = 3006048 }, + { url = "https://files.pythonhosted.org/packages/cc/d6/f5239c913e24cc5edcdf51edffdfe240123042902892770038172ba5529f/typos-1.29.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:211ac5179c4c0309f6c486b475cd47ff732df2f5a87b0c940ff906cd432ecf5c", size = 7604008 }, + { url = "https://files.pythonhosted.org/packages/a1/3a/f3a328244572e1d04a2a2ed91f0544011cfbcc1395a519384b3652c7b620/typos-1.29.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:98f2aa90baa26ba511106ff3bd46e13cdf1c327167159faf6bb8950eb22cc748", size = 6840196 }, + { url = "https://files.pythonhosted.org/packages/0b/dc/938fd703d41f92ecff5e244727876242e48b68280f7da0bfb434a4543b6c/typos-1.29.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24faf9854f317ec1037c72d42821146260a1e1d2deeb6d0eed85a90c56808564", size = 7511353 }, + { url = "https://files.pythonhosted.org/packages/a8/97/70529576271229cf004099eb8eb2ace251b872e2803cceb49f0ca4791b7e/typos-1.29.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7f0d3fce6ed2fe738e5346cface35d59f1583e2957464b68eb014f861e06c11e", size = 6672024 }, + { url = "https://files.pythonhosted.org/packages/3c/89/4b07711d4f12f7812adc6b1c2ce5abaa1d1e304bd3004415590edc3013b7/typos-1.29.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f32f63b7d96c47d749981d7d666978504f73d25704ff27dcb500c516fb86e585", size = 7534929 }, + { url = "https://files.pythonhosted.org/packages/c5/00/caf8ed43d79251347714c6a884ff624db0528dc961ca085162fed0d00ad3/typos-1.29.9-py3-none-win32.whl", hash = "sha256:059fc40f7e830b7dabad5bc2e311872ba7dfb6cdc4dc8227330c81f61bbdc52f", size = 2741271 }, + { url = "https://files.pythonhosted.org/packages/5a/a6/c5884b7dcbcab379dd59973bfb3c91223f8528d2c86565aea68046852b6f/typos-1.29.9-py3-none-win_amd64.whl", hash = "sha256:9f618a399ff2ff144b97c0a867f0439c26394f8849853f2f68339fce5b3bae02", size = 2891412 }, +] + +[[package]] +name = "tzdata" +version = "2025.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/0f/fa4723f22942480be4ca9527bbde8d43f6c3f2fe8412f00e7f5f6746bc8b/tzdata-2025.1.tar.gz", hash = "sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694", size = 194950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/dd/84f10e23edd882c6f968c21c2434fe67bd4a528967067515feca9e611e5e/tzdata-2025.1-py2.py3-none-any.whl", hash = "sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639", size = 346762 }, +] + +[[package]] +name = "untokenize" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/46/e7cea8159199096e1df52da20a57a6665da80c37fb8aeb848a3e47442c32/untokenize-0.1.1.tar.gz", hash = "sha256:3865dbbbb8efb4bb5eaa72f1be7f3e0be00ea8b7f125c69cbd1f5fda926f37a2", size = 3099 } + +[[package]] +name = "urllib3" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, +] + +[[package]] +name = "virtualenv" +version = "20.29.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/88/dacc875dd54a8acadb4bcbfd4e3e86df8be75527116c91d8f9784f5e9cab/virtualenv-20.29.2.tar.gz", hash = "sha256:fdaabebf6d03b5ba83ae0a02cfe96f48a716f4fae556461d180825866f75b728", size = 4320272 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/fa/849483d56773ae29740ae70043ad88e068f98a6401aa819b5d6bee604683/virtualenv-20.29.2-py3-none-any.whl", hash = "sha256:febddfc3d1ea571bdb1dc0f98d7b45d24def7428214d4fb73cc486c9568cce6a", size = 4301478 }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079 }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076 }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077 }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077 }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065 }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070 }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067 }, +] + +[[package]] +name = "wcmatch" +version = "10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bracex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/ab/b3a52228538ccb983653c446c1656eddf1d5303b9cb8b9aef6a91299f862/wcmatch-10.0.tar.gz", hash = "sha256:e72f0de09bba6a04e0de70937b0cf06e55f36f37b3deb422dfaf854b867b840a", size = 115578 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/df/4ee467ab39cc1de4b852c212c1ed3becfec2e486a51ac1ce0091f85f38d7/wcmatch-10.0-py3-none-any.whl", hash = "sha256:0dd927072d03c0a6527a20d2e6ad5ba8d0380e60870c383bc533b71744df7b7a", size = 39347 }, +] + +[[package]] +name = "wcwidth" +version = "0.2.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 }, +] + +[[package]] +name = "xsdata" +version = "24.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/82/c2b73724bdd31f81e263e1f87e4b9bd70703dd4282ce44ffe1978cda541b/xsdata-24.12.tar.gz", hash = "sha256:67c16c032bbde82c064f6c28cdf6b4f8adcbd0498e2668a346ff8e52a3a200b4", size = 343596 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/e5/70026869f5ab3c6be413ab5a978849cfe2037f2e82bd1da8550c7627caa3/xsdata-24.12-py3-none-any.whl", hash = "sha256:2656cfa7240d215a4f2ff30abeebee9afad1e72b805f3ed29ded518a7d3785ee", size = 232617 }, +] + +[package.optional-dependencies] +cli = [ + { name = "click" }, + { name = "click-default-group" }, + { name = "docformatter" }, + { name = "jinja2" }, + { name = "ruff" }, + { name = "toposort" }, +] +lxml = [ + { name = "lxml" }, +] +soap = [ + { name = "requests" }, +]