diff --git a/.editorconfig b/.editorconfig index 3facc24..8cbbc66 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,20 +12,11 @@ indent_size = 4 indent_style = space indent_size = 2 -[*.cfg] -indent_style = space -indent_size = 2 - [*.sh] -indent_style = space indent_size = 4 -[Makefile] -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = false -indent_style = tab +[justfile] +indent_size = 2 [*.md] trim_trailing_whitespace = false diff --git a/.github/workflows/api-quality.tpl.yml b/.github/workflows/api-quality.tpl.yml new file mode 100644 index 0000000..e914b7a --- /dev/null +++ b/.github/workflows/api-quality.tpl.yml @@ -0,0 +1,144 @@ +--- +name: "Template: Run code checks on Python project" + +on: + workflow_call: + inputs: + ## General + working-directory: + required: true + type: string + description: | + "Directory in which the terraform project is located" + python-version: + required: false + default: "3.12" + type: string + description: | + "Python version to use" + run-tests: + required: false + type: boolean + description: | + "Run tests before zipping the lambda" + default: true + +permissions: + id-token: write + contents: read + pull-requests: write + +jobs: + check: + runs-on: ubuntu-latest + + defaults: + run: + working-directory: ${{ inputs.working-directory }} + + services: + postgres: + image: postgres:17.5 + env: + POSTGRES_DB: app + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + POSTGRES_PORT: 5432 + options: >- + --health-cmd "pg_isready --dbname=app --username=postgres" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + id: setup-python + with: + version: 0.7.5 + python-version: ${{ inputs.python-version }} + enable-cache: true + cache-dependency-glob: | + **/uv.lock + **/pyproject.toml + + - name: Cache hit + run: echo '${{ steps.setup-python.outputs.cache-hit }}' # true if cache-hit occured on the primary key + + - name: Install CI dependencies + run: uv sync --all-extras + + - name: Run migrations + run: uv run app database upgrade head --no-prompt + + - name: pytest + id: pytest + if: ${{ inputs.run-tests }} + run: uv run pytest + continue-on-error: true + + - name: PyTest Failure + if: inputs.run-tests && steps.pytest.outcome == 'failure' + uses: mshick/add-pr-comment@v2 + with: + message-id: check-zip-lambda-pytest-${{ inputs.working-directory }} + repo-token: ${{ secrets.GITHUB_TOKEN }} + message: | + #### PyTest failed for **${{ inputs.working-directory }}**: + + ``` + ${{ steps.pytest.outputs.stdout }} + ``` + + - name: mypy + id: mypy + run: uv run mypy . + continue-on-error: true + + - name: Mypy Failure + if: steps.mypy.outcome == 'failure' + uses: mshick/add-pr-comment@v2 + with: + message-id: check-zip-lambda-mypy-${{ inputs.working-directory }} + allow-repeats: true + repo-token: ${{ secrets.GITHUB_TOKEN }} + message: | + #### Mypy failed for **${{ inputs.working-directory }}**: + ``` + ${{ steps.mypy.outputs.stdout }} + ``` + ``` + ${{ steps.mypy.outputs.stderr }} + ``` + + - name: ruff check + id: ruff + run: uv run ruff check --output-format=github --fix . + continue-on-error: true + + - name: Ruff Failure + if: steps.ruff.outcome == 'failure' + uses: mshick/add-pr-comment@v2 + with: + message-id: check-zip-lambda-ruff-${{ inputs.working-directory }} + repo-token: ${{ secrets.GITHUB_TOKEN }} + message: | + #### Ruff failed for **${{ inputs.working-directory }}**: + ``` + ${{ steps.ruff.outputs.stdout }} + ``` + ``` + ${{ steps.ruff.outputs.stderr }} + ``` + + - name: Errors Found + if: >- + steps.pytest.outcome == 'failure' || + steps.mypy.outcome == 'failure' || + steps.ruff.outcome == 'failure' + run: exit 1 diff --git a/.github/workflows/deployment.tpl.yml b/.github/workflows/deployment.tpl.yml new file mode 100644 index 0000000..77a0aa0 --- /dev/null +++ b/.github/workflows/deployment.tpl.yml @@ -0,0 +1,43 @@ +--- +name: "Template: deployment" + +on: + workflow_call: + inputs: + dry-run: + required: false + type: boolean + description: | + "Run the workflow in dry-run mode" + default: true + +jobs: + # We do it this way to avoid checking for changes in each job + changes: + name: "Check for changes" + runs-on: ubuntu-latest + # Set job outputs to values from filter step + outputs: + api: ${{ steps.filter.outputs.api }} + + steps: + - uses: actions/checkout@v4 + + - uses: dorny/paths-filter@v3 + id: filter + with: + base: main + filters: >- + api: + - api/**/* + - .github/workflows/python-* + + api-quality: + name: "API: Quality" + needs: changes + if: >- + needs.changes.outputs.api == 'true' + uses: ./.github/workflows/api-quality.tpl.yml + secrets: inherit + with: + working-directory: api diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..f784784 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,25 @@ +--- +name: Development branch + +concurrency: + group: ${{ github.ref }}-development + cancel-in-progress: false + +permissions: + id-token: write + contents: read + pull-requests: write + packages: write + +on: + workflow_dispatch: + push: + branches: + - main + +jobs: + deploy: + uses: ./.github/workflows/deployment.tpl.yml + secrets: inherit + with: + dry-run: false diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml new file mode 100644 index 0000000..f01c7ec --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,25 @@ +--- +name: Pull request checks + +concurrency: + group: ${{ github.ref }}-pull-request + cancel-in-progress: false + +permissions: + id-token: write + contents: read + pull-requests: write + packages: write + +on: + workflow_dispatch: + pull_request: + branches: + - main + +jobs: + deploy: + uses: ./.github/workflows/deployment.tpl.yml + secrets: inherit + with: + dry-run: true diff --git a/.github/workflows/python-quality.tpl.yml b/.github/workflows/python-quality.tpl.yml new file mode 100644 index 0000000..f412414 --- /dev/null +++ b/.github/workflows/python-quality.tpl.yml @@ -0,0 +1,124 @@ +--- +name: "Template: Run code checks on Python project" + +on: + workflow_call: + inputs: + ## General + working-directory: + required: true + type: string + description: | + "Directory in which the terraform project is located" + python-version: + required: false + default: "3.12" + type: string + description: | + "Python version to use" + run-tests: + required: false + type: boolean + description: | + "Run tests before zipping the lambda" + default: true + +permissions: + id-token: write + contents: read + pull-requests: write + +jobs: + check: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ${{ inputs.working-directory }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + id: setup-python + with: + version: 0.7.5 + python-version: ${{ inputs.python-version }} + enable-cache: true + cache-dependency-glob: | + **/uv.lock + **/pyproject.toml + + - name: Cache hit + run: echo '${{ steps.setup-python.outputs.cache-hit }}' # true if cache-hit occured on the primary key + + - name: Install CI dependencies + run: uv sync --all-extras + + - name: pytest + id: pytest + if: ${{ inputs.run-tests }} + run: uv run pytest --cov -vvv + continue-on-error: true + + - name: PyTest Failure + if: inputs.run-tests && steps.pytest.outcome == 'failure' + uses: mshick/add-pr-comment@v2 + with: + message-id: check-zip-lambda-pytest-${{ inputs.working-directory }} + repo-token: ${{ secrets.GITHUB_TOKEN }} + message: | + #### PyTest failed for **${{ inputs.working-directory }}**: + + ``` + ${{ steps.pytest.outputs.stdout }} + ``` + + - name: mypy + id: mypy + run: uv run mypy . + continue-on-error: true + + - name: Mypy Failure + if: steps.mypy.outcome == 'failure' + uses: mshick/add-pr-comment@v2 + with: + message-id: check-zip-lambda-mypy-${{ inputs.working-directory }} + allow-repeats: true + repo-token: ${{ secrets.GITHUB_TOKEN }} + message: | + #### Mypy failed for **${{ inputs.working-directory }}**: + ``` + ${{ steps.mypy.outputs.stdout }} + ``` + ``` + ${{ steps.mypy.outputs.stderr }} + ``` + + - name: ruff check + id: ruff + run: uv run ruff check --output-format=github --fix . + continue-on-error: true + + - name: Ruff Failure + if: steps.ruff.outcome == 'failure' + uses: mshick/add-pr-comment@v2 + with: + message-id: check-zip-lambda-ruff-${{ inputs.working-directory }} + repo-token: ${{ secrets.GITHUB_TOKEN }} + message: | + #### Ruff failed for **${{ inputs.working-directory }}**: + ``` + ${{ steps.ruff.outputs.stdout }} + ``` + ``` + ${{ steps.ruff.outputs.stderr }} + ``` + + - name: Errors Found + if: >- + steps.pytest.outcome == 'failure' || + steps.mypy.outcome == 'failure' || + steps.ruff.outcome == 'failure' + run: exit 1 diff --git a/.gitignore b/.gitignore index ad008a1..bef15d9 100644 --- a/.gitignore +++ b/.gitignore @@ -70,3 +70,6 @@ devdoc/ *~ *# *retry + +.env* +.python-version \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 556d291..0000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,37 +0,0 @@ ---- -fail_fast: true -repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.2.1 - hooks: - - id: check-ast - - id: check-symlinks - - id: check-executables-have-shebangs - - id: check-merge-conflict - - id: check-json - - id: check-yaml - - id: detect-private-key - - id: double-quote-string-fixer - - id: trailing-whitespace - - id: no-commit-to-branch # No (direct) commits to master - - repo: https://github.com/asottile/add-trailing-comma - rev: v1.0.0 - hooks: - - id: add-trailing-comma - - repo: https://github.com/pre-commit/mirrors-isort - rev: v4.3.18 - hooks: - - id: isort - - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.1.6 - hooks: - - id: forbid-crlf - files: \.md$ - - id: remove-crlf - files: \.md$ - - repo: local - hooks: - - id: lint - name: tox lint - entry: tox -e lint - language: system diff --git a/CHANGES.rst b/CHANGES.rst index 04493bf..3414df7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,13 @@ Changelog ========= +Version 0.5.0 (18.06.2025) +-------------------------- +- Drop support for Python 3.6. +- Add support for Python 3.11 and 3.12. +- Update dependencies to latest versions. + + Version 0.4.4 (12.08.2019) -------------------------- - Fix bug to correctly call `call_command` in `pdf_utls.pdf_to_cmyk`. diff --git a/ci-scripts/bump-python-version.sh b/ci-scripts/bump-python-version.sh new file mode 100755 index 0000000..591eba4 --- /dev/null +++ b/ci-scripts/bump-python-version.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +SCRIPT_PATH=$(dirname "$0") +source "${SCRIPT_PATH}"/common.sh + +version=$1 +echo "new version ${version}" +( +cat <lambda_handlers/version.py diff --git a/ci-scripts/common.sh b/ci-scripts/common.sh new file mode 100755 index 0000000..9968304 --- /dev/null +++ b/ci-scripts/common.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash + +# Begin Standard 'imports' +set -e +set -o pipefail + +# Logging, loosely based on http://www.ludovicocaldara.net/dba/bash-tips-4-use-logging-levels/ +gray="\\e[37m" +blue="\\e[36m" +red="\\e[31m" +green="\\e[32m" +reset="\\e[0m" + +info() { echo -e "${blue}INFO: $*${reset}"; } +error() { echo -e "${red}ERROR: $*${reset}"; } +debug() { + if [[ "${DEBUG}" == "true" ]]; then + echo -e "${gray}DEBUG: $*${reset}" + fi +} + +success() { echo -e "${green}✔ $*${reset}"; } +fail() { + echo -e "${red}✖ $*${reset}" + exit 1 +} + +## Enable debug mode. +enable_debug() { + if [[ "${DEBUG}" == "true" ]]; then + info "Enabling debug mode." + set -x + fi +} + +# Execute a command, saving its output and exit status code, and echoing its output upon completion. +# Globals set: +# status: Exit status of the command that was executed. +# output: Output generated from the command. +# +run() { + echo "$@" + set +e + output=$("$@" 2>&1) + status=$? + set -e + echo "${output}" +} + +# End standard 'imports' diff --git a/ci-scripts/pypi-release.sh b/ci-scripts/pypi-release.sh new file mode 100755 index 0000000..6578f2a --- /dev/null +++ b/ci-scripts/pypi-release.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +SCRIPT_PATH=$(dirname "$0") +source "${SCRIPT_PATH}"/common.sh + +parse_environment_variables() { + export TWINE_USERNAME=${PYPI_USER:?'PYPI_USER variable missing.'} + export TWINE_PASSWORD=${PYPI_PASS:?'PYPI_PASS variable missing.'} +} + +pypi_release() { + make release +} + +enable_debug +parse_environment_variables +pypi_release diff --git a/docstamp/cli/__init__.py b/docstamp/cli/__init__.py index 4b7029b..e69de29 100644 --- a/docstamp/cli/__init__.py +++ b/docstamp/cli/__init__.py @@ -1 +0,0 @@ -from .cli import cli diff --git a/docstamp/cli/cli.py b/docstamp/cli/cli.py deleted file mode 100644 index db9154c..0000000 --- a/docstamp/cli/cli.py +++ /dev/null @@ -1,159 +0,0 @@ -#!python -import click - -import os -import math -import logging - -from docstamp.file_utils import get_extension -from docstamp.template import TextDocument -from docstamp.config import LOGGING_LVL - -from docstamp.cli.utils import ( - CONTEXT_SETTINGS, - verbose_switch, - get_items_from_csv, - ExistingFilePath, - DirPath -) - -ACCEPTED_DOCS = "Inkscape (.svg), PDFLatex (.tex), XeLatex (.tex)" - - -# declare the CLI group -@click.group(context_settings=CONTEXT_SETTINGS) -def cli(): - pass - - -@cli.command(context_settings=CONTEXT_SETTINGS) -@click.option('-i', '--input', type=ExistingFilePath, required=False, - help='Path to the CSV file with the data elements to be used to ' - 'fill the template. This file must have the same fields as ' - 'the template file.') -@click.option('-t', '--template', type=ExistingFilePath, required=True, - help='Template file path. The extension of this file will be ' - 'used to determine what software to use to render the ' - 'documents: \n' + ACCEPTED_DOCS) -@click.option('-f', '--field', type=str, multiple=True, - help='The field or fields that will be used to name ' - 'the output files. Use many of this to declare many values. ' - 'Otherwise files will be numbered.') -@click.option('-o', '--outdir', type=DirPath, default='stamped', - show_default=True, help='Output folder path.') -@click.option('-p', '--prefix', type=str, - help='Output files prefix. Default: Template file name.') -@click.option('-d', '--otype', type=click.Choice(['pdf', 'png', 'svg']), - default='pdf', show_default=True, - help='Output file type.') -@click.option('-c', '--command', type=click.Choice(['inkscape', 'pdflatex', 'xelatex']), - default='inkscape', show_default=True, - help='The rendering command to be used in case file name ' - 'extension is not specific.') -@click.option('--index', type=int, multiple=True, - help='Index/es of the CSV file that you want to create the ' - 'document from. Note that the samples numbers start from 0 ' - 'and the empty ones do not count.') -@click.option('--dpi', type=int, default=150, help='Output file resolution') -@click.option('-v', '--verbose', is_flag=True, - help='Output debug logs.') -@click.option('-u', '--unicode_support', is_flag=True, default=False, - help='Allows unicode characters to be correctly encoded in the PDF.') -def create(input, template, field, outdir, prefix, otype, command, index, - dpi, verbose, unicode_support): - """Use docstamp to create documents from the content of a CSV file or - a Google Spreadsheet. - - Examples: \n - docstamp create -i badge.csv -t badge_template.svg -o badges - docstamp create -i badge.csv -t badge_template.svg -o ./badges -d pdf - """ - logging.basicConfig(level=LOGGING_LVL) - log = logging.getLogger(__name__) - - # setup verbose mode - verbose_switch(verbose) - - input_file = input - fields = field - - # init set of template contents - log.debug('Reading CSV elements from {}.'.format(input_file)) - items, fieldnames = get_items_from_csv(input_file) - - # check if got any item - if len(items) == 0: - click.echo('Quiting because found 0 items.') - exit(-1) - - if not fields: - # set the number of zeros that the files will have - n_zeros = int(math.floor(math.log10(len(items))) + 1) - else: - # check that fields has all valid fields - for field_name in fields: - if field_name not in fieldnames: - raise ValueError('Field name {} not found in input file ' - ' header.'.format(field_name)) - - # filter the items if index - if index: - myitems = {idx: items[idx] for idx in index} - items = myitems - log.debug('Using the elements with index {} of the input ' - 'file.'.format(index)) - - # make output folder - if not os.path.exists(outdir): - os.mkdir(outdir) - - # create template document model - log.debug('Creating the template object using the file {}.'.format(template)) - template_doc = TextDocument.from_template_file(template, command) - log.debug('Created an object of type {}.'.format(type(template_doc))) - - # let's stamp them! - for idx in items: - item = items[idx] - - if not len(fields): - file_name = str(idx).zfill(n_zeros) - else: - field_values = [] - try: - for field_name in fields: - field_values.append(item[field_name].replace(' ', '')) - except: - log.exception('Could not get field {} value from' - ' {}'.format(field_name, item)) - exit(-1) - else: - file_name = '_'.join(field_values) - - log.debug('Filling template {} with values of item {}.'.format(file_name, idx)) - try: - template_doc.fill(item) - except: - log.exception('Error filling document for {}th item'.format(idx)) - continue - - # set output file path - file_extension = get_extension(template) - if prefix is None: - basename = os.path.basename(template).replace(file_extension, '') - - file_name = basename + '_' + file_name - file_path = os.path.join(outdir, file_name + '.' + otype) - - kwargs = {'file_type': otype, - 'dpi': dpi, - 'support_unicode': unicode_support} - - log.debug('Rendering file {}.'.format(file_path)) - try: - template_doc.render(file_path, **kwargs) - except: - log.exception('Error creating {} for {}.'.format(file_path, item)) - exit(-1) - else: - log.debug('Successfully rendered {}.'.format(file_path)) diff --git a/docstamp/cli/main.py b/docstamp/cli/main.py new file mode 100644 index 0000000..991bcda --- /dev/null +++ b/docstamp/cli/main.py @@ -0,0 +1,219 @@ +#!python +from __future__ import annotations + +import logging +import math +import sys +from pathlib import Path + +import click + +from docstamp.cli.utils import ( + CONTEXT_SETTINGS, + DirPath, + ExistingFilePath, + get_items_from_csv, +) +from docstamp.file_utils import get_extension +from docstamp.template import TextDocument + +ACCEPTED_DOCS = "Inkscape (.svg), PDFLatex (.tex), XeLatex (.tex)" + + +# declare the CLI group +@click.group(context_settings=CONTEXT_SETTINGS) +def cli(): + pass + + +@cli.command(context_settings=CONTEXT_SETTINGS) +@click.option( + "-i", + "--input", + type=ExistingFilePath, + required=False, + help="Path to the CSV file with the data elements to be used to " + "fill the template. This file must have the same fields as " + "the template file.", +) +@click.option( + "-t", + "--template", + type=ExistingFilePath, + required=True, + help="Template file path. The extension of this file will be " + "used to determine what software to use to render the " + "documents: \n" + ACCEPTED_DOCS, +) +@click.option( + "-f", + "--field", + type=str, + multiple=True, + help="The field or fields that will be used to name " + "the output files. Use many of this to declare many values. " + "Otherwise files will be numbered.", +) +@click.option( + "-o", + "--outdir", + type=DirPath, + default="stamped", + show_default=True, + help="Output folder path.", +) +@click.option( + "-p", "--prefix", type=str, help="Output files prefix. Default: Template file name." +) +@click.option( + "-d", + "--otype", + type=click.Choice(["pdf", "png", "svg"]), + default="pdf", + show_default=True, + help="Output file type.", +) +@click.option( + "-c", + "--command", + type=click.Choice(["inkscape", "pdflatex", "xelatex"]), + default="inkscape", + show_default=True, + help="The rendering command to be used in case file name " + "extension is not specific.", +) +@click.option( + "--index", + type=int, + multiple=True, + help="Index/es of the CSV file that you want to create the " + "document from. Note that the samples numbers start from 0 " + "and the empty ones do not count.", +) +@click.option("--dpi", type=int, default=150, help="Output file resolution") +@click.option("-v", "--verbose", is_flag=True, help="Output debug logs.") +@click.option( + "-u", + "--unicode_support", + is_flag=True, + default=False, + help="Allows unicode characters to be correctly encoded in the PDF.", +) +def create( # noqa: C901, PLR0912, PLR0913, PLR0915 + input, + template, + field, + outdir, + prefix, + otype, + command, + index, + dpi, + verbose, + unicode_support, +): + """Use docstamp to create documents from the content of a CSV file. + + Examples: \n + docstamp create -i badge.csv -t badge_template.svg -o badges + docstamp create -i badge.csv -t badge_template.svg -o ./badges -d pdf + """ + logging.basicConfig(level="INFO") + log = logging.getLogger(__name__) + + # setup verbose mode + if verbose: + log_level = logging.DEBUG + else: + log_level = logging.INFO + + logging.getLogger().setLevel(log_level) + + input_file = input + fields = field + + # init set of template contents + log.debug("Reading CSV elements from %s.", input_file) + items, header_fields = get_items_from_csv(input_file) + if not header_fields: + raise ValueError( + f"Could not read the header from '{input_file}'. " + "Please check the input file format." + ) + + # check if got any item + if len(items) == 0: + click.echo("Quiting because found 0 items.") + sys.exit(-1) + + if not fields: + # set the number of zeros that the file name will have + n_zeros = int(math.floor(math.log10(len(items))) + 1) + else: + # check that fields has all valid fields + for field_name in fields: + if field_name not in header_fields: + raise ValueError( + f"Field name {field_name} not found in input file header." + ) + + # filter the items if index + if index: + picked_items = {idx: items[idx] for idx in index} + items = picked_items + log.debug("Using the elements with index %s of the input file.", index) + + # make output folder + output_directory = Path(outdir) + output_directory.mkdir(parents=True, exist_ok=True) + + # create template document model + log.debug("Creating the template object using the file %s.", template) + template_doc = TextDocument.from_template_file(template, command) + log.debug("Created an object of type %s.", type(template_doc)) + + # let's stamp them! + for idx in items: + item = items[idx] + + if not fields: + file_suffix = str(idx).zfill(n_zeros) + else: + field_values = [] + try: + for field_name in fields: + field_values.append(item[field_name].replace(" ", "")) + except KeyError as _: + log.exception("Could not get field %s value from %s.", field_name, item) + sys.exit(-1) + else: + file_suffix = "_".join(field_values) + + log.debug("Filling template %s with values of item %s.", file_suffix, idx) + try: + template_doc.render(item) + except Exception as error: + log.exception("Error filling document for %sth item: %s.", idx, error) + continue + + # set output file path + if prefix is None: + file_extension = get_extension(template) + basename = Path(template).name.replace(file_extension, "") + else: + basename = prefix + + file_path = output_directory / f"{basename}_{file_suffix}.{otype}" + log.debug("Rendering file %s.", file_path) + try: + template_doc.export( + file_path=file_path, + file_type=otype, + dpi=dpi, + support_unicode=unicode_support, + ) + except Exception as error: + log.exception("Error creating %s for %s: %s.", file_path, item, error) + sys.exit(-1) + else: + log.debug("Successfully rendered %s.", file_path) diff --git a/docstamp/cli/utils.py b/docstamp/cli/utils.py index 51e52f3..980eb90 100644 --- a/docstamp/cli/utils.py +++ b/docstamp/cli/utils.py @@ -1,20 +1,24 @@ -# -*- coding: utf-8 -*- """ Utilities for the CLI functions. """ + +from __future__ import annotations + +import os import re -import json -import logging from csv import DictReader +from typing import TYPE_CHECKING import click +import orjson -from docstamp.model import json_to_dict +if TYPE_CHECKING: + from collections.abc import Sequence + from typing import Any # different context options -CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) -UNKNOWN_OPTIONS = dict(allow_extra_args=True, - ignore_unknown_options=True) +CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]} +UNKNOWN_OPTIONS = {"allow_extra_args": True, "ignore_unknown_options": True} # specification of existing ParamTypes DirPath = click.Path(file_okay=False, resolve_path=True) @@ -23,52 +27,35 @@ UnexistingFilePath = click.Path(dir_okay=False, resolve_path=True) -# validators -def check_not_none(ctx, param, value): - if value is None: - raise click.BadParameter('got {}.'.format(value)) - return value - - -# declare custom click.ParamType class RegularExpression(click.ParamType): - name = 'regex' + """Regular expression parameter type for Click.""" + + name = "regex" - def convert(self, value, param, ctx): + def convert(self, value: str, param: str, ctx: click.Context | None) -> re.Pattern: # type: ignore[return] + """Convert the value to a compiled regular expression pattern.""" try: - rex = re.compile(value, re.IGNORECASE) + return re.compile(value, re.IGNORECASE) except ValueError: - self.fail('%s is not a valid regular expression.' % value, param, ctx) - else: - return rex - + self.fail(f"Invalid regular expression: {value}.", param, ctx) -# other utilities -def echo_list(alist): - for i in alist: - click.echo(i) - -def get_items_from_csv(csv_filepath): +def get_items_from_csv( + csv_filepath: os.PathLike | str, +) -> tuple[dict[int, Any], Sequence[str] | None]: + """ + Read a CSV file and return its contents as a dictionary of enumerated items + and a list of the header column names. + """ # CSV to JSON # one JSON object for each item items = {} - with open(str(csv_filepath), 'r') as csvfile: - + with open(str(csv_filepath)) as csvfile: reader = DictReader(csvfile) for idx, row in enumerate(reader): - item = json_to_dict(json.dumps(row)) - if any([item[i] != '' for i in item]): + item = orjson.loads(orjson.dumps(row)).decode("utf-8") + if any(item): items[idx] = item return items, reader.fieldnames - - -def verbose_switch(verbose=False): - if verbose: - log_level = logging.DEBUG - else: - log_level = logging.INFO - - logging.getLogger().setLevel(log_level) diff --git a/docstamp/collections.py b/docstamp/collections.py deleted file mode 100644 index 83b7ebc..0000000 --- a/docstamp/collections.py +++ /dev/null @@ -1,41 +0,0 @@ -# -*- coding: utf-8 -*- - -import pickle - - -class Enum(set): - def __getattr__(self, name): - if name in self: - return name - raise AttributeError - - -class ItemSet(object): - - def __iter__(self): - return self.items.__iter__() - - def __next__(self): - return self.items.__next__() - - def next(self): - return self.items.next() - - def __getitem__(self, item): - if hasattr(self.items, '__getitem__'): - return self.items[item] - else: - raise AttributeError('Item set has no __getitem__ implemented.') - - def __len__(self): - return len(self.items) - - def save(self, file_path): - with open(file_path, 'wb'): - pickle.dump(self.__dict__, file_path, pickle.HIGHEST_PROTOCOL) - - def load_from_pickle(self, file_path): - with open(file_path, 'rb'): - adict = pickle.load(file_path) - pickle.dump(self.__dict__, file_path, pickle.HIGHEST_PROTOCOL) - self.__dict__.update(adict) diff --git a/docstamp/commands.py b/docstamp/commands.py index 90bba37..947c536 100644 --- a/docstamp/commands.py +++ b/docstamp/commands.py @@ -1,79 +1,21 @@ -# coding=utf-8 -# ------------------------------------------------------------------------------- -# Author: Alexandre Manhaes Savio -# Grupo de Inteligencia Computational -# Universidad del Pais Vasco UPV/EHU -# -# 2015, Alexandre Manhaes Savio -# Use this at your own risk! -# ------------------------------------------------------------------------------- +from __future__ import annotations +import logging import os -import sys import shutil -import logging import subprocess +from pathlib import Path from subprocess import CalledProcessError log = logging.getLogger(__name__) -def simple_call(cmd_args): - return subprocess.call(' '.join(cmd_args), shell=True) - - -def is_exe(fpath): - """Return True if fpath is an executable file path. - - Parameters - ---------- - fpath: str - File path - - Returns - ------- - is_executable: bool - """ - return os.path.isfile(fpath) and os.access(fpath, os.X_OK) - - -def which(cmd_name): - """Returns the absolute path of the given CLI program name.""" - if sys.version_info > (3, 0): - return which_py3(cmd_name) - else: - # Python 2 code in this block - return which_py2(cmd_name) - - -def which_py3(cmd_name): - return shutil.which(cmd_name) - - -def which_py2(cmd_name): - fpath, fname = os.path.split(cmd_name) - if fpath: - if is_exe(cmd_name): - return cmd_name - else: - for path in os.environ["PATH"].split(os.pathsep): - path = path.strip('"') - exe_file = os.path.join(path, cmd_name) - if is_exe(exe_file): - return exe_file - - return None - - -def check_command(cmd_name): - """ Raise a FileNotFoundError if the command is not found. - :param cmd_name: - """ - if which(cmd_name) is None: - raise FileNotFoundError('Could not find command named {}.'.format(cmd_name)) +def simple_call(cmd_args: list[str]) -> int: + """Call a command with arguments and returns its return value.""" + return subprocess.call(" ".join(cmd_args), shell=True) # noqa: S602 -def call_command(cmd_name, args_strings): +def call_command(cmd_name: str | os.PathLike, args_strings: list[str]) -> int: """Call CLI command with arguments and returns its return value. Parameters @@ -89,20 +31,26 @@ def call_command(cmd_name, args_strings): return_value Command return value. """ - if not os.path.isabs(cmd_name): - cmd_fullpath = which(cmd_name) + cmd_path = Path(cmd_name) + cmd_fullpath: str | os.PathLike | None = None + if not cmd_path.is_absolute(): + cmd_fullpath = shutil.which(cmd_name) else: cmd_fullpath = cmd_name + if cmd_fullpath is None: + raise FileNotFoundError(f"Command {cmd_name} not found in PATH.") + try: - cmd_line = [cmd_fullpath] + args_strings - log.debug('Calling: `{}`.'.format(' '.join(cmd_line))) - # retval = subprocess.check_call(cmd_line) - retval = subprocess.call(' '.join(cmd_line), shell=True) - except CalledProcessError as ce: + cmd_line = [str(cmd_fullpath), *args_strings] + shell_command = " ".join(cmd_line) + log.debug("Calling: `%s`.", shell_command) + retval = subprocess.call(shell_command, shell=True) # noqa: S602 + except CalledProcessError as error: log.exception( - "Error calling command with arguments: " - "{} \n With return code: {}".format(cmd_line, ce.returncode) + "Error calling command with arguments: " "%s \n With return code: %s", + cmd_line, + error.returncode, ) raise else: diff --git a/docstamp/config.py b/docstamp/config.py index 27b5da5..80d819a 100644 --- a/docstamp/config.py +++ b/docstamp/config.py @@ -1,25 +1,13 @@ -# coding=utf-8 -# ------------------------------------------------------------------------------- -# Author: Alexandre Manhaes Savio -# Grupo de Inteligencia Computational -# Universidad del Pais Vasco UPV/EHU -# -# 2015, Alexandre Manhaes Savio -# Use this at your own risk! -# ------------------------------------------------------------------------------- +from __future__ import annotations import os import re -import logging +import shutil +from pathlib import Path from sys import platform as _platform -from docstamp.commands import which, is_exe -LOGGING_LVL = logging.INFO -logging.basicConfig(level=LOGGING_LVL) - - -def find_file_match(folder_path, regex=''): +def find_file_match(folder_path: Path, regex: str = ".*") -> list[Path]: """ Returns absolute paths of files that match the regex within folder_path and all its children folders. @@ -29,7 +17,8 @@ def find_file_match(folder_path, regex=''): Parameters ---------- - folder_path: string + folder_path: Path + The folder path to search in. regex: string @@ -39,47 +28,53 @@ def find_file_match(folder_path, regex=''): """ outlist = [] - for root, dirs, files in os.walk(folder_path): - outlist.extend([os.path.join(root, f) for f in files - if re.match(regex, f)]) + for root, _, files in folder_path.walk(): + outlist.extend([Path(root) / f for f in files if re.match(regex, f)]) return outlist -def get_system_path(): +def get_other_program_folders() -> list[Path]: + """Return a list of common program folders based on the platform.""" if _platform == "linux" or _platform == "linux2": - return os.environ['PATH'] + return [ + Path("/opt/bin"), + Path("/usr/local/bin"), + Path("/usr/bin"), + Path("/bin"), + Path("/usr/sbin"), + Path("/sbin"), + ] elif _platform == "darwin": - return os.environ['PATH'] + return [Path("/Applications"), Path(os.environ["HOME"]) / "Applications"] elif _platform == "win32": # don't know if this works - return os.environ['PATH'] - - -def get_other_program_folders(): - if _platform == "linux" or _platform == "linux2": - return ['/opt/bin'] - elif _platform == "darwin": - return ['/Applications', os.path.join(os.environ['HOME'], 'Applications')] - elif _platform == "win32": - # don't know if this works - return ['C:\Program Files'] + return [Path(r"C:\Program Files")] + else: + raise NotImplementedError( + f"Platform {_platform} is not supported for finding other program folders." + ) -def get_temp_dir(): +def get_temp_dir() -> Path | None: + """Return the temporary directory based on the platform.""" if _platform == "linux" or _platform == "linux2": - return '/tmp' + return Path("/tmp") elif _platform == "darwin": - return '.' + return Path.cwd() elif _platform == "win32": # don't know if this works return None + else: + raise NotImplementedError( + f"Platform {_platform} is not supported for getting the temporary directory." + ) -def find_in_other_programs_folders(app_name): - app_name_regex = '^' + app_name + '$' +def find_in_other_programs_folders(app_name: str) -> Path | None: + """Search for the application binary in common program folders.""" + app_name_regex = f"^{app_name}$" other_folders = get_other_program_folders() - for folder in other_folders: abin_file = find_program(folder, app_name_regex) if abin_file is not None: @@ -88,72 +83,70 @@ def find_in_other_programs_folders(app_name): return None -def find_program(root_dir, exec_name): +def find_program(root_dir: Path, exec_name: str) -> Path | None: + """Find the executable file in the given directory and its subdirectories.""" file_matches = find_file_match(root_dir, exec_name) for f in file_matches: - if is_exe(f): + if is_executable(f): return f return None -def ask_for_path_of(app_name): - bin_path = None - while bin_path is not None: - bin_path = input('Insert path of {} executable file [Press Ctrl+C to exit]: '.format(app_name)) - - if not os.path.exists(bin_path): - print('Could not find file {}. Try it again.'.format(bin_path)) - bin_path = None - continue - - if not is_exe(bin_path): - print('No execution permissions on file {}. Try again.'.format(bin_path)) - bin_path = None - continue - - return bin_path +def is_executable(filepath: str | Path) -> bool: + """Check if the given file is executable.""" + filepath = Path(filepath) + if not filepath.exists(): + return False + if _platform in ("linux", "linux2", "darwin"): + return filepath.is_file() and os.access(filepath, os.X_OK) + elif _platform == "win32": + return filepath.suffix.lower() in (".exe", ".bat", ".cmd") + else: + raise NotImplementedError( + f"Platform {_platform} is not supported for checking executable files." + ) -def proactive_search_of(app_name): - if _platform == 'win32': - bin_name = app_name + '.exe' +def get_executable_path(app_name: str) -> Path | None: + """Search for the binary of the given application. + This function checks the system PATH, common program folders. + Parameters + ---------- + app_name: str + The name of the application to search for. + Returns + ------- + Path | None + The path to the binary if found, otherwise None. + """ + if _platform == "win32": + bin_name = app_name + ".exe" else: bin_name = app_name - bin_path = which(app_name) - if bin_path is not None and is_exe(bin_path): - return bin_path + which_result = shutil.which(cmd=app_name) + if which_result is not None and is_executable(filepath=which_result): + return Path(which_result) - bin_path = find_in_other_programs_folders(bin_name) - if bin_path is not None: - return bin_path + search_result = find_in_other_programs_folders(app_name=bin_name) + if search_result is not None: + return search_result - return ask_for_path_of(bin_name) + raise FileNotFoundError( + f"Could not find {app_name} binary in the system PATH or common program folders. " + "Please provide the path manually." + ) -def get_inkscape_binpath(): - bin_name = 'inkscape' +def get_inkscape_binpath() -> Path | None: + """Return the Inkscape binary path.""" + bin_name = "inkscape" if _platform == "darwin": - bin_name = 'inkscape-bin' - - if 'INKSCAPE_BINPATH' not in globals(): - global INKSCAPE_BINPATH - INKSCAPE_BINPATH = proactive_search_of(bin_name) - - return INKSCAPE_BINPATH - - -def get_lyx_binpath(): - if 'LYX_BINPATH' not in globals(): - global LYX_BINPATH - LYX_BINPATH = proactive_search_of('lyx') - return LYX_BINPATH - -# TEMPLATES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'templates') + bin_name = "inkscape-bin" + return get_executable_path(bin_name) -# JINJA_ENV = Environment(loader=PackageLoader('docstamp', 'templates')) -# JINJA_ENV = Environment(loader=FileSystemLoader(TEMPLATES_DIR)) -# FILE_EXPORTERS = {'.svg': Inkscape,} -# '.tex': PdfLatex, -# '.lyx': LyX} +def get_lyx_binpath() -> Path | None: + """Return the LyX binary path.""" + bin_name = "lyx" + return get_executable_path(bin_name) diff --git a/docstamp/data_source.py b/docstamp/data_source.py deleted file mode 100644 index a84d6df..0000000 --- a/docstamp/data_source.py +++ /dev/null @@ -1,39 +0,0 @@ -import sys - -from .unicode_csv import UnicodeWriter - -if sys.version_info[0] >= 3: - raw_input = input - - -class GoogleData: - - def getCSV(self): - """ - Returns - ------- - filename: str - """ - import getpass - import gspread - - user = raw_input("Insert Google username:") - password = getpass.getpass(prompt="Insert password:") - name = raw_input("SpreadSheet filename on Drive:") - sheet = raw_input("Sheet name (first sheet is default):") - - cl = gspread.login(user, password) - sh = cl.open(name) - - if not (sheet.strip()): - ws = sh.sheet1 - sheet = "1" - else: - ws = sh.worksheet(sheet) - - filename = name + '-worksheet_' + sheet + '.csv' - with open(filename, 'wb') as f: - writer = UnicodeWriter(f) - writer.writerows(ws.get_all_values()) - - return filename diff --git a/docstamp/exceptions.py b/docstamp/exceptions.py new file mode 100644 index 0000000..5cbf848 --- /dev/null +++ b/docstamp/exceptions.py @@ -0,0 +1,17 @@ +"""A module to hold custom exceptions for the docstamp package.""" + + +class RenderingError(Exception): + """Exception raised when there is an error rendering a document template.""" + + +class ExportError(Exception): + """Exception raised when there is an error exporting a document template.""" + + +class QRCodeError(Exception): + """Exception raised when there is an error generating or saving a QR code.""" + + +class FileDeletionError(Exception): + """Exception raised when there is an error deleting a file.""" diff --git a/docstamp/file_utils.py b/docstamp/file_utils.py index 72932b9..9dba59c 100644 --- a/docstamp/file_utils.py +++ b/docstamp/file_utils.py @@ -1,82 +1,35 @@ -# coding=utf-8 -# ------------------------------------------------------------------------------- -# Author: Alexandre Manhaes Savio -# Grupo de Inteligencia Computational -# Universidad del Pais Vasco UPV/EHU -# -# 2015, Alexandre Manhaes Savio -# Use this at your own risk! -# ------------------------------------------------------------------------------- +from __future__ import annotations import os import tempfile -import logging -from glob import glob +from pathlib import Path from docstamp.config import get_temp_dir +from docstamp.exceptions import FileDeletionError -log = logging.getLogger(__name__) - -def get_extension(filepath, check_if_exists=False): +def get_extension(filepath: os.PathLike | str) -> str: """Return the extension of fpath. Parameters ---------- - fpath: string - File name or path - - check_if_exists: bool + filepath: string + File name or path Returns ------- str The extension of the file name or path """ - if check_if_exists: - if not os.path.exists(filepath): - err = 'File not found: ' + filepath - log.error(err) - raise IOError(err) - try: - rest, ext = os.path.splitext(filepath) + ext = "".join(Path(filepath).suffixes[-2:]) except: raise else: return ext -def add_extension_if_needed(filepath, ext, check_if_exists=False): - """Add the extension ext to fpath if it doesn't have it. - - Parameters - ---------- - filepath: str - File name or path - - ext: str - File extension - - check_if_exists: bool - - Returns - ------- - File name or path with extension added, if needed. - """ - if not filepath.endswith(ext): - filepath += ext - - if check_if_exists: - if not os.path.exists(filepath): - err = 'File not found: ' + filepath - log.error(err) - raise IOError(err) - - return filepath - - -def remove_ext(filepath): +def remove_ext(filepath: os.PathLike | str) -> str: """Removes the extension of the file. Parameters @@ -89,11 +42,17 @@ def remove_ext(filepath): str File path or name without extension """ - return filepath[:filepath.rindex(get_extension(filepath))] + extension = get_extension(filepath) + if not extension: + return str(filepath) + return str(filepath).removesuffix(extension) -def get_tempfile(suffix='.txt', dirpath=None): - """ Return a temporary file with the given suffix within dirpath. +def get_tempfile( + suffix: str = ".txt", + dirpath: os.PathLike | str | None = None, +) -> tempfile._TemporaryFileWrapper[bytes]: + """Return a temporary file with the given suffix within dirpath. If dirpath is None, will look for a temporary folder in your system. Parameters @@ -112,11 +71,11 @@ def get_tempfile(suffix='.txt', dirpath=None): if dirpath is None: dirpath = get_temp_dir() - return tempfile.NamedTemporaryFile(suffix=suffix, dir=dirpath) + return tempfile.NamedTemporaryFile(suffix=suffix, dir=str(dirpath)) -def cleanup(workdir, extension): - """ Remove the files in workdir that have the given extension. +def cleanup(workdir: os.PathLike | str, extension: str): + """Remove the files in workdir that have the given extension. Parameters ---------- @@ -126,77 +85,18 @@ def cleanup(workdir, extension): extension: str File extension without the dot, e.g., 'txt' """ - [os.remove(f) for f in glob(os.path.join(workdir, '*.' + extension))] - - -def mkdir(dirpath): - """Create a folder in `dirpath` if it does'nt exist.""" - if not os.path.exists(dirpath): - os.mkdir(dirpath) - - -def csv_to_json(csv_filepath, json_filepath, fieldnames, ignore_first_line=True): - """ Convert a CSV file in `csv_filepath` into a JSON file in `json_filepath`. - - Parameters - ---------- - csv_filepath: str - Path to the input CSV file. - - json_filepath: str - Path to the output JSON file. Will be overwritten if exists. - - fieldnames: List[str] - Names of the fields in the CSV file. - - ignore_first_line: bool - """ - import csv - import json - - csvfile = open(csv_filepath, 'r') - jsonfile = open(json_filepath, 'w') + cleanup_target = Path(workdir) + for f in cleanup_target.glob("*." + extension): + try: + f.unlink() + except OSError as exc: + raise FileDeletionError( + f"Error trying to delete file {f} in {workdir}." + ) from exc - reader = csv.DictReader(csvfile, fieldnames) - rows = [] - if ignore_first_line: - next(reader) - for row in reader: - rows.append(row) - - json.dump(rows, jsonfile) - jsonfile.close() - csvfile.close() - - -def write_to_file(file_path, content, encoding=None): - """ Write `content` inside the file in `file_path` with the given encoding. - Parameters - ---------- - file_path: str - Path to the output file. Will be overwritten if exists. - - content: str - The content you want in the file. - - encoding: str - The name of the encoding. - """ - try: - # TODO: check if in Python2 this should be this way - # it's possible that we have to make this function more complex - # to check type(content) and depending on that set 'w' without enconde - # or 'wb' with encode. - with open(file_path, "wb") as f: - f.write(content.encode(encoding)) - except: - log.exception('Error writing to file in {}'.format(file_path)) - raise - - -def replace_file_content(filepath, old, new, max=1): - """ Modify the content of `filepath`, replacing `old` for `new`. +def replace_file_content(filepath: os.PathLike | str, old: str, new: str, max: int = 1): + """Modify the content of `filepath`, replacing `old` for `new`. Parameters ---------- @@ -212,18 +112,7 @@ def replace_file_content(filepath, old, new, max=1): max: int If larger than 0, Only the first `max` occurrences are replaced. """ - with open(filepath, 'r') as f: - content = f.read() - + _filepath = Path(filepath) + content = _filepath.read_text() content = content.replace(old, new, max) - with open(filepath, 'w') as f: - f.write(content) - - -def cleanup_docstamp_output(output_dir=''): - """ Remove the 'tmp*.aux', 'tmp*.out' and 'tmp*.log' files in `output_dir`. - :param output_dir: - """ - suffixes = ['aux', 'out', 'log'] - files = [f for suf in suffixes for f in glob(os.path.join(output_dir, 'tmp*.{}'.format(suf)))] - [os.remove(file) for file in files] + _filepath.write_text(content) diff --git a/docstamp/inkscape.py b/docstamp/inkscape.py index a72f9ac..8271748 100644 --- a/docstamp/inkscape.py +++ b/docstamp/inkscape.py @@ -1,24 +1,19 @@ -# coding=utf-8 -# ------------------------------------------------------------------------------- -# Author: Alexandre Manhaes Savio -# Grupo de Inteligencia Computational -# Universidad del Pais Vasco UPV/EHU -# -# 2015, Alexandre Manhaes Savio -# Use this at your own risk! -# ------------------------------------------------------------------------------- +"""A module to handle Inkscape CLI commands for SVG and PDF conversions.""" + +from __future__ import annotations import os -import logging +from pathlib import Path -from docstamp.config import get_inkscape_binpath from docstamp.commands import call_command +from docstamp.config import get_inkscape_binpath from docstamp.svg_utils import rsvg_export -log = logging.getLogger(__name__) - -def call_inkscape(args_strings, inkscape_binpath=None): +def call_inkscape( + args_strings: list[str], + inkscape_binpath: os.PathLike | str | None = None, +) -> int: """Call inkscape CLI with arguments and returns its return value. Parameters @@ -32,21 +27,25 @@ def call_inkscape(args_strings, inkscape_binpath=None): return_value Inkscape command CLI call return value. """ - log.debug('Looking for the binary file for inkscape.') - if inkscape_binpath is None: inkscape_binpath = get_inkscape_binpath() - if inkscape_binpath is None or not os.path.exists(inkscape_binpath): - raise IOError( - 'Inkscape binary has not been found. Please check configuration.' + if inkscape_binpath is None or not Path(inkscape_binpath).exists(): + raise FileNotFoundError( + "Inkscape binary has not been found. Please check configuration." ) - return call_command(inkscape_binpath, args_strings) + return call_command(cmd_name=inkscape_binpath, args_strings=args_strings) -def inkscape_export(input_file, output_file, export_flag="-A", dpi=90, inkscape_binpath=None): - """ Call Inkscape to export the input_file to output_file using the +def inkscape_export( + input_file: os.PathLike | str, + output_file: os.PathLike | str, + export_flag: str = "-A", + dpi: int = 90, + inkscape_binpath: os.PathLike | str | None = None, +) -> int: + """Call Inkscape to export the input_file to output_file using the specific export argument flag for the output file type. Parameters @@ -61,42 +60,73 @@ def inkscape_export(input_file, output_file, export_flag="-A", dpi=90, inkscape_ export_flag: str Inkscape CLI flag to indicate the type of the output file + dpi: int + Dots per inch for the output file. Default is 90. + + inkscape_binpath: str | None + Path to the Inkscape command binary. + If None, it will try to find the binary in your computer. + Returns ------- return_value Command call return value """ - if not os.path.exists(input_file): - log.error('File {} not found.'.format(input_file)) - raise IOError((0, 'File not found.', input_file)) - - if '=' not in export_flag: - export_flag += ' ' - - arg_strings = [] - arg_strings += ['--without-gui'] - arg_strings += ['--export-text-to-path'] - arg_strings += ['{}"{}"'.format(export_flag, output_file)] - arg_strings += ['--export-dpi={}'.format(dpi)] - arg_strings += ['"{}"'.format(input_file)] - - return call_inkscape(arg_strings, inkscape_binpath=inkscape_binpath) - - -def svg2pdf(svg_file_path, pdf_file_path, dpi=150, command_binpath=None, support_unicode=False): - """ Transform SVG file to PDF file - """ + if not Path(input_file).exists(): + raise FileNotFoundError(f"File {input_file} not found.") + + if "=" not in export_flag: + export_flag += " " + + args = [ + "--without-gui", + "--export-text-to-path", + "--export-pdf-version=1.5", + f'{export_flag}"{output_file}"', + f"--export-dpi={dpi}", + f'"{input_file}"', + ] + return call_inkscape(args_strings=args, inkscape_binpath=inkscape_binpath) + + +def svg2pdf( + svg_file_path: os.PathLike | str, + pdf_file_path: os.PathLike | str, + dpi: int = 150, + command_binpath: os.PathLike | str | None = None, + support_unicode: bool = False, +): + """Transform SVG file to PDF file""" if support_unicode: - return rsvg_export(svg_file_path, pdf_file_path, dpi=dpi, rsvg_binpath=command_binpath) - - return inkscape_export(svg_file_path, pdf_file_path, export_flag="-A", - dpi=dpi, inkscape_binpath=command_binpath) - + return rsvg_export( + input_file=svg_file_path, + output_file=pdf_file_path, + dpi=dpi, + rsvg_binpath=command_binpath, + ) -def svg2png(svg_file_path, png_file_path, dpi=150, inkscape_binpath=None): - """ Transform SVG file to PNG file - """ - return inkscape_export(svg_file_path, png_file_path, export_flag="-e", - dpi=dpi, inkscape_binpath=inkscape_binpath) + return inkscape_export( + input_file=svg_file_path, + output_file=pdf_file_path, + export_flag="-A", + dpi=dpi, + inkscape_binpath=command_binpath, + ) + + +def svg2png( + svg_file_path: os.PathLike | str, + png_file_path: os.PathLike | str, + dpi: int = 150, + inkscape_binpath: os.PathLike | str | None = None, +) -> int: + """Transform SVG file to PNG file""" + return inkscape_export( + input_file=svg_file_path, + output_file=png_file_path, + export_flag="-e", + dpi=dpi, + inkscape_binpath=inkscape_binpath, + ) diff --git a/docstamp/model.py b/docstamp/model.py deleted file mode 100644 index 456cffa..0000000 --- a/docstamp/model.py +++ /dev/null @@ -1,76 +0,0 @@ -# coding=utf-8 -# ------------------------------------------------------------------------------- -# Author: Alexandre Manhaes Savio -# Grupo de Inteligencia Computational -# Universidad del Pais Vasco UPV/EHU -# -# 2015, Alexandre Manhaes Savio -# Use this at your own risk! -# ------------------------------------------------------------------------------- -import json - - -def translate_key_values(adict, translations, default=''): - """Modify the keys in adict to the ones in translations. - Be careful, this will modify your input dictionary. - The keys not present in translations will be left intact. - - Parameters - ---------- - adict: a dictionary - - translations: iterable of 2-tuples - Each 2-tuple must have the following format: - (, ) - - Returns - ------- - Translated adict - """ - for src_key, dst_key in translations: - adict[dst_key] = adict.pop(src_key, default) - return adict - - -def json_to_dict(json_str): - """Convert json string into dict""" - return json.JSONDecoder().decode(json_str) - - -class JSONMixin(object): - """Simple, stateless json utilities mixin. - - Requires class to implement two methods: - to_json(self): convert data to json-compatible datastructure (dict, - list, strings, numbers) - @classmethod from_json(cls, json): load data from json-compatible structure. - """ - - @classmethod - def from_json_str(cls, json_str): - """Convert json string representation into class instance. - - Args: - json_str: json representation as string. - - Returns: - New instance of the class with data loaded from json string. - """ - dct = json_to_dict(json_str) - return cls(**dct) - - def to_json_str(self): - """Convert data to json string representation. - - Returns: - json representation as string. - """ - adict = dict(vars(self), sort_keys=True) - adict['type'] = self.__class__.__name__ - return json.dumps(adict) - - def __repr__(self): - return self.to_json_str() - - def __str__(self): - return self.to_json_str() diff --git a/docstamp/pdf_utils.py b/docstamp/pdf_utils.py index 8b65ab6..750cd0d 100644 --- a/docstamp/pdf_utils.py +++ b/docstamp/pdf_utils.py @@ -1,13 +1,20 @@ -""" -Function helpers to manage PDF files. -""" +"""Function helpers to manage PDF files.""" + +from __future__ import annotations + +import os +from pathlib import Path + from PyPDF2 import PdfFileMerger, PdfFileReader -from docstamp.commands import call_command +from .commands import call_command -def merge_pdfs(pdf_filepaths, out_filepath): - """ Merge all the PDF files in `pdf_filepaths` in a new PDF file `out_filepath`. +def merge_pdfs( + pdf_filepaths: list[os.PathLike | str], + out_filepath: str | os.PathLike, +) -> Path: + """Merge all the PDF files in `pdf_filepaths` in a new PDF file `out_filepath`. Parameters ---------- @@ -24,30 +31,36 @@ def merge_pdfs(pdf_filepaths, out_filepath): """ merger = PdfFileMerger() for pdf in pdf_filepaths: - merger.append(PdfFileReader(open(pdf, 'rb'))) - - merger.write(out_filepath) + pdf_filepath = Path(pdf) + merger.append(PdfFileReader(pdf_filepath.open("rb"))) - return out_filepath + merger.write(str(out_filepath)) + return Path(out_filepath) -def pdf_to_cmyk(input_file, output_file): - """ User `gs` (Ghostscript) to convert the colore model of a PDF to CMYK. +def pdf_to_cmyk(input_file: os.PathLike | str, output_file: os.PathLike | str) -> int: + """Use `gs` (Ghostscript) to convert the colour model of a PDF to CMYK + for printing. Parameters ---------- input_file: str output_file: str + + Returns + ------- + exit_code: int + The exit code of the `gs` command call. """ cmd_args = [ - '-dSAFER', - '-dBATCH', - '-dNOPAUSE', - '-dNOCACHE', - '-sDEVICE=pdfwrite', - '-sColorConversionStrategy=CMYK', - '-dProcessColorModel=/DeviceCMYK', - '-sOutputFile="{}" "{}"'.format(output_file, input_file), + "-dSAFER", + "-dBATCH", + "-dNOPAUSE", + "-dNOCACHE", + "-sDEVICE=pdfwrite", + "-sColorConversionStrategy=CMYK", + "-dProcessColorModel=/DeviceCMYK", + f'-sOutputFile="{output_file}" "{input_file}"', ] - call_command('gs', cmd_args) + return call_command("gs", cmd_args) diff --git a/docstamp/pdflatex.py b/docstamp/pdflatex.py index e20bcd5..d9efc34 100644 --- a/docstamp/pdflatex.py +++ b/docstamp/pdflatex.py @@ -1,25 +1,50 @@ -# coding=utf-8 -# ------------------------------------------------------------------------------- -# Author: Alexandre Manhaes Savio -# Grupo de Inteligencia Computational -# Universidad del Pais Vasco UPV/EHU -# -# 2015, Alexandre Manhaes Savio -# Use this at your own risk! -# ------------------------------------------------------------------------------- +"""LaTeX to PDF conversion helpers.""" +from __future__ import annotations + +import logging import os import shutil -import logging +from pathlib import Path +from typing import Literal -from docstamp.commands import simple_call, check_command -from docstamp.file_utils import remove_ext, cleanup +from docstamp.commands import simple_call +from docstamp.file_utils import cleanup, remove_ext log = logging.getLogger(__name__) -def tex2pdf(tex_file, output_file=None, output_format='pdf'): - """ Call PDFLatex to convert TeX files to PDF. +def _check_latex_file_inputs( + tex_file_path: Path, + output_file_path: Path | None = None, + output_format: Literal["dvi", "pdf"] = "pdf", +) -> None: + """Check the inputs for LaTeX file conversion functions.""" + if not tex_file_path.exists(): + raise FileNotFoundError(f"Could not find file {tex_file_path}.") + + if output_format not in ("pdf", "dvi"): + raise ValueError( + f"Invalid output format given {output_format}. Can only accept 'pdf' or 'dvi'." + ) + + if output_file_path is not None and output_file_path.exists(): + raise FileExistsError(f"Output file {output_file_path} already exists.") + + +def _cleanup_aux_log_files(workdir: Path) -> None: + """Remove auxiliary and log files from the working directory.""" + log.debug("Cleaning *.aux and *.log files from folder %s.", workdir) + cleanup(workdir=workdir, extension="aux") + cleanup(workdir=workdir, extension="log") + + +def tex2pdf( + tex_file: os.PathLike | str, + output_file: os.PathLike | str | None = None, + output_format: Literal["dvi", "pdf"] = "pdf", +) -> int: + """Call PDFLatex to convert TeX files to PDF. Parameters ---------- @@ -35,45 +60,52 @@ def tex2pdf(tex_file, output_file=None, output_format='pdf'): Returns ------- - return_value - PDFLatex command call return value. + exit_code: int + The exit code of the PDFLatex command call. """ - if not os.path.exists(tex_file): - raise IOError('Could not find file {}.'.format(tex_file)) - - if output_format != 'pdf' and output_format != 'dvi': - raise ValueError("Invalid output format given {}. Can only accept 'pdf' or 'dvi'.".format(output_format)) - - cmd_name = 'pdflatex' - check_command(cmd_name) + tex_file_path = Path(tex_file) + output_file_path = Path(output_file) if output_file else None + _check_latex_file_inputs( + tex_file_path=tex_file_path, + output_file_path=output_file_path, + output_format=output_format, + ) + + cmd_name = "pdflatex" + if shutil.which(cmd=cmd_name) is None: + raise FileNotFoundError(f"Could not find command named {cmd_name}.") args_strings = [cmd_name] - if output_file is not None: - args_strings += ['-output-directory="{}" '.format(os.path.abspath(os.path.dirname(output_file)))] - - result_dir = os.path.dirname(output_file) if output_file else os.path.dirname(tex_file) + result_dir = tex_file_path.parent + if output_file_path is not None: + output_dir = output_file_path.parent.absolute() + args_strings += [f'-output-directory="{output_dir}"'] + result_dir = output_file_path.parent - args_strings += ['-output-format="{}"'.format(output_format)] - args_strings += ['"' + tex_file + '"'] + args_strings += [f'-output-format="{output_format}"'] + args_strings += [f'"{tex_file}"'] - log.debug('Calling command {} with args: {}.'.format(cmd_name, args_strings)) - ret = simple_call(args_strings) + log.debug("Calling command %s with args: %s.", cmd_name, args_strings) + exit_code = simple_call(args_strings) - result_file = os.path.join(result_dir, remove_ext(os.path.basename(tex_file)) + '.' + output_format) - if os.path.exists(result_file): - shutil.move(result_file, output_file) - else: - raise IOError('Could not find PDFLatex result file.') + tex_file_name = f"{remove_ext(tex_file_path.name)}.{output_format}" + result_file = result_dir / tex_file_name + if not result_file.exists(): + raise FileNotFoundError("Could not find PDFLatex result file.") - log.debug('Cleaning *.aux and *.log files from folder {}.'.format(result_dir)) - cleanup(result_dir, 'aux') - cleanup(result_dir, 'log') + if output_file_path is not None: + shutil.move(result_file, output_file_path) - return ret + _cleanup_aux_log_files(workdir=result_dir) + return exit_code -def xetex2pdf(tex_file, output_file=None, output_format='pdf'): - """ Call XeLatex to convert TeX files to PDF. +def xetex2pdf( + tex_file: os.PathLike | str, + output_file: os.PathLike | str | None = None, + output_format: Literal["pdf", "dvi"] = "pdf", +) -> int: + """Call XeLatex to convert TeX files to PDF. Parameters ---------- @@ -89,38 +121,43 @@ def xetex2pdf(tex_file, output_file=None, output_format='pdf'): Returns ------- - return_value - XeLatex command call return value. + exit_code: int + The exit code of the XeLatex command call. """ - if not os.path.exists(tex_file): - raise IOError('Could not find file {}.'.format(tex_file)) - - if output_format != 'pdf' and output_format != 'dvi': - raise ValueError("Invalid output format given {}. Can only accept 'pdf' or 'dvi'.".format(output_format)) - - cmd_name = 'xelatex' - check_command(cmd_name) + tex_file_path = Path(tex_file) + output_file_path = Path(output_file) if output_file else None + _check_latex_file_inputs( + tex_file_path=tex_file_path, + output_file_path=output_file_path, + output_format=output_format, + ) + + cmd_name = "xelatex" + if shutil.which(cmd=cmd_name) is None: + raise FileNotFoundError(f"Could not find command named {cmd_name}.") args_strings = [cmd_name] - if output_file is not None: - args_strings += ['-output-directory="{}"'.format(os.path.abspath(os.path.dirname(output_file)))] + result_dir = tex_file_path.parent + if output_file_path is not None: + output_dir = output_file_path.parent.absolute() + args_strings += [f'-output-directory="{output_dir}"'] + result_dir = output_file_path.parent + + if output_format == "dvi": + args_strings += ["-no-pdf"] - if output_format == 'dvi': - args_strings += ['-no-pdf'] + args_strings += [f'"{tex_file}"'] - result_dir = os.path.dirname(output_file) if output_file else os.path.dirname(tex_file) - args_strings += ['"' + tex_file + '"'] + log.debug("Calling command %s with args: %s.", cmd_name, args_strings) + exit_code = simple_call(args_strings) - log.debug('Calling command {} with args: {}.'.format(cmd_name, args_strings)) - ret = simple_call(args_strings) + tex_file_name = f"{remove_ext(tex_file_path.name)}.{output_format}" + result_file = result_dir / tex_file_name + if not result_file.exists(): + raise FileNotFoundError("Could not find XeLatex result file.") - result_file = os.path.join(result_dir, remove_ext(os.path.basename(tex_file)) + '.pdf') - if os.path.exists(result_file): - shutil.move(result_file, output_file) - else: - raise IOError('Could not find PDFLatex result file.') + if output_file_path is not None: + shutil.move(result_file, output_file_path) - log.debug('Cleaning *.aux and *.log files from folder {}.'.format(result_dir)) - cleanup(result_dir, 'aux') - cleanup(result_dir, 'log') - return ret + _cleanup_aux_log_files(workdir=result_dir) + return exit_code diff --git a/docstamp/qrcode.py b/docstamp/qrcode.py index 1e0ce45..43eadb1 100644 --- a/docstamp/qrcode.py +++ b/docstamp/qrcode.py @@ -1,58 +1,82 @@ -""" -Utility functions to create QRCodes using `qrcode`. -""" +"""Utility functions to create QRCodes using `qrcode`.""" + +from __future__ import annotations + +import os + import qrcode import qrcode.image.svg -from docstamp.file_utils import replace_file_content +from .exceptions import QRCodeError +from .file_utils import replace_file_content -def save_into_qrcode(text, out_filepath, color='', box_size=10, pixel_size=1850): - """ Save `text` in a qrcode svg image file. +def _create_qrcode_image( + text: str, + box_size: float = 10, +) -> qrcode.image.svg.SvgPathImage: + """Create a QR code image from `text`. Parameters ---------- text: str The string to be codified in the QR image. - - out_filepath: str - Path to the output file - - color: str - A RGB color expressed in 6 hexadecimal values. - - box_size: scalar + box_size: float Size of the QR code boxes. + Returns + ------- + qrcode.image.svg.SvgPathImage + The QR code image object. + Raises + ------ + QRCodeError + If there is an error trying to generate the QR code image. """ try: - qr = qrcode.QRCode(version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, - box_size=box_size, border=0, ) + qr = qrcode.QRCode( + version=1, + error_correction=qrcode.constants.ERROR_CORRECT_L, + box_size=box_size, + border=0, + ) qr.add_data(text) qr.make(fit=True) + return qr.make_image(image_factory=qrcode.image.svg.SvgPathImage) except Exception as exc: - raise Exception('Error trying to generate QR code ' - ' from `vcard_string`: {}'.format(text)) from exc - else: - img = qr.make_image(image_factory=qrcode.image.svg.SvgPathImage) - - _ = _qrcode_to_file(img, out_filepath) + raise QRCodeError( + f"Error trying to generate QR code from `vcard_string`: {text}" + ) from exc - if color: - replace_file_content(out_filepath, 'fill:#000000', 'fill:#{}'.format(color)) +def save_into_qrcode( + text: str, + out_filepath: os.PathLike | str, + color: str = "", + box_size: float = 10, +) -> None: + """Save `text` in a qrcode svg image file. -def _qrcode_to_file(qrcode, out_filepath): - """ Save a `qrcode` object into `out_filepath`. Parameters ---------- - qrcode: qrcode object + text: str + The string to be codified in the QR image. out_filepath: str - Path to the output file. + Path to the output file + + color: str + A RGB color expressed in 6 hexadecimal values. + + box_size: scalar + Size of the QR code boxes. """ + img = _create_qrcode_image(text=text, box_size=box_size) try: - qrcode.save(out_filepath) + img.save(out_filepath) except Exception as exc: - raise IOError('Error trying to save QR code file {}.'.format(out_filepath)) from exc + raise RuntimeError( + f"Error trying to save QR code file {out_filepath}." + ) from exc else: - return qrcode + if color: + replace_file_content(out_filepath, "fill:#000000", f"fill:#{color}") diff --git a/docstamp/svg_fonts.py b/docstamp/svg_fonts.py index 660fe3c..62866a9 100644 --- a/docstamp/svg_fonts.py +++ b/docstamp/svg_fonts.py @@ -1,104 +1,151 @@ -# coding=utf-8 -# ------------------------------------------------------------------------------- -# Author: Alexandre Manhaes Savio -# Asociación Python San Sebastián (ACPySS) -# -# 2017, Alexandre Manhaes Savio -# Use this at your own risk! -# ------------------------------------------------------------------------------- +"""SVG font embedding helpers.""" +from __future__ import annotations -import os import base64 +import os +from pathlib import Path +from typing import TYPE_CHECKING + from lxml import etree from .file_utils import get_extension -FONT_TYPES = {'ttf': 'truetype', - 'otf': 'opentype'} +if TYPE_CHECKING: + from typing import Literal -def get_base64_encoding(bin_filepath): +FONT_TYPES: dict[str, Literal["truetype", "opentype"]] = { + "ttf": "truetype", + "otf": "opentype", +} + + +def get_base64_encoding(bin_filepath: os.PathLike | str) -> bytes: """Return the base64 encoding of the given binary file""" - return base64.b64encode(open(bin_filepath, 'r').read()) + _bin_filepath = Path(bin_filepath) + return base64.b64encode(_bin_filepath.open(mode="rb").read()) -def remove_ext(filepath): +def remove_ext(filepath: os.PathLike | str) -> str: """Return the basename of filepath without extension.""" - return os.path.basename(filepath).split('.')[0] + _filepath = Path(filepath) + return _filepath.name.split(".")[0] + +class FontFace: + """CSS font-face object -class FontFace(object): - """CSS font-face object""" + Represents a font-face object that can be used in CSS. + It contains the font file path, font type, name, and provides + methods to generate the CSS text for embedding the font in a web page. + + Parameters + ---------- + filepath: str or Path + The path to the font file (e.g., .ttf or .otf). + + fonttype: str, optional + The type of the font (e.g., 'truetype' or 'opentype'). + If not provided, it will be inferred from the file extension. + + name: str, optional + The name of the font. If not provided, it will be derived + from the file name without extension. + """ - def __init__(self, filepath, fonttype=None, name=None): - self.filepath = filepath + def __init__( + self, + filepath: os.PathLike | str, + fonttype: Literal["ttf", "otf"] | None = None, + name: str | None = None, + ): + self.filepath = Path(filepath) self.ftype = fonttype self.given_name = name @classmethod - def from_file(cls, filepath): + def from_file(cls, filepath: os.PathLike | str) -> FontFace: + """Create a FontFace instance from a file path.""" return cls(filepath) @property - def name(self): + def name(self) -> str: + """Return the name of the font.""" if self.given_name is None: - return remove_ext(self.filepath) + return remove_ext(filepath=self.filepath) else: return self.given_name @property - def base64(self): - return get_base64_encoding(self.filepath) + def base64(self) -> bytes: + """Return the base64 encoding of the font file.""" + return get_base64_encoding(bin_filepath=self.filepath) @property - def fonttype(self): + def fonttype(self) -> Literal["truetype", "opentype"]: + """Return the font type based on the file extension.""" if self.ftype is None: - return FONT_TYPES[get_extension(self.filepath)] + file_extension = get_extension(filepath=self.filepath) + if file_extension not in FONT_TYPES: + raise ValueError( + f"Unsupported font type for file {self.filepath}. " + "Supported types are: " + ", ".join(FONT_TYPES.keys()) + ) + return FONT_TYPES[file_extension] else: - return self.ftype + return FONT_TYPES[self.ftype] @property - def ext(self): - return get_extension(self.filepath) + def ext(self) -> str: + """Return the file extension of the font file.""" + return get_extension(filepath=self.filepath) @property - def css_text(self): - css_text = u"@font-face{\n" - css_text += u"font-family: " + self.name + ";\n" - css_text += u"src: url(data:font/" + self.ext + ";" - css_text += u"base64," + self.base64 + ") " - css_text += u"format('" + self.fonttype + "');\n}\n" + def css_text(self) -> str: + """Return the CSS text for embedding the font.""" + css_text = "@font-face" + css_text += "{\n" + css_text += f"font-family: {self.name};\n" + css_text += f"src: url(data:font/{self.ext};base64,{self.base64!r}) " + css_text += f"format('{self.fonttype}');\n" + css_text += "}\n" return css_text -class FontFaceGroup(object): +class FontFaceGroup: """Group of FontFaces""" - def __init__(self): - self.fontfaces = [] + def __init__(self, fontfaces: list[FontFace] | None = None): + self.fontfaces: list[FontFace] = fontfaces or [] @property - def css_text(self): - css_text = u'' + css_text += "" return css_text @property - def xml_elem(self): + def xml_elem(self) -> etree.Element: + """Return the XML element for the CSS text.""" return etree.fromstring(self.css_text) - def append(self, font_face): + def append(self, font_face) -> None: + """Append a FontFace to the group.""" self.fontfaces.append(font_face) -def _embed_font_to_svg(filepath, font_files): - """ Return the ElementTree of the SVG content in `filepath` +def _embed_font_to_svg( + filepath: os.PathLike | str, font_files: list[os.PathLike | str] | None = None +) -> etree.ElementTree: + """Return the ElementTree of the SVG content in `filepath` with the font content embedded. """ - with open(filepath, 'r') as svgf: + _filepath = Path(filepath) + with _filepath.open() as svgf: tree = etree.parse(svgf) if not font_files: @@ -109,7 +156,7 @@ def _embed_font_to_svg(filepath, font_files): fontfaces.append(FontFace(font_file)) for element in tree.iter(): - if element.tag.split("}")[1] == 'svg': + if element.tag.split("}")[1] == "svg": break element.insert(0, fontfaces.xml_elem) @@ -117,8 +164,12 @@ def _embed_font_to_svg(filepath, font_files): return tree -def embed_font_to_svg(filepath, outfile, font_files): - """ Write ttf and otf font content from `font_files` +def embed_font_to_svg( + filepath: os.PathLike | str, + outfile: os.PathLike | str, + font_files: list[os.PathLike | str] | None = None, +) -> None: + """Write ttf and otf font content from `font_files` in the svg file in `filepath` and write the result in `outfile`. @@ -133,5 +184,5 @@ def embed_font_to_svg(filepath, outfile, font_files): font_files: iterable of str List of paths to .ttf or .otf files. """ - tree = _embed_font_to_svg(filepath, font_files) - tree.write(outfile, encoding='utf-8', pretty_print=True) + tree = _embed_font_to_svg(filepath=filepath, font_files=font_files) + tree.write(outfile, encoding="utf-8", pretty_print=True) diff --git a/docstamp/svg_utils.py b/docstamp/svg_utils.py index f408695..3c4f892 100644 --- a/docstamp/svg_utils.py +++ b/docstamp/svg_utils.py @@ -1,17 +1,19 @@ -""" -Function helpers to do stuff on svg files. -""" +"""Function helpers to do stuff on svg files.""" + +from __future__ import annotations + import os -import logging +import shutil +from pathlib import Path -from docstamp.commands import call_command, which, check_command +import svgutils import svgutils.transform as sg -log = logging.getLogger(__name__) +from docstamp.commands import call_command -def replace_chars_for_svg_code(svg_content): - """ Replace known special characters to SVG code. +def replace_chars_for_svg_code(svg_content: str) -> str: + """Replace known special characters to SVG code. Parameters ---------- @@ -24,10 +26,10 @@ def replace_chars_for_svg_code(svg_content): """ result = svg_content svg_char = [ - ('&', '&'), - ('>', '>'), - ('<', '<'), - ('"', '"'), + ("&", "&"), + (">", ">"), + ("<", "<"), + ('"', """), ] for c, entity in svg_char: @@ -36,8 +38,8 @@ def replace_chars_for_svg_code(svg_content): return result -def _check_svg_file(svg_file): - """ Try to read a SVG file if `svg_file` is a string. +def _check_svg_file(svg_file: str | svgutils.SVGFigure) -> svgutils.SVGFigure: + """Try to read a SVG file if `svg_file` is a string. Raise an exception in case of error or return the svg object. If `svg_file` is a svgutils svg object, will just return it. @@ -58,20 +60,26 @@ def _check_svg_file(svg_file): """ if isinstance(svg_file, str): try: - svg = sg.fromfile(svg_file) + return sg.fromfile(svg_file) except Exception as exc: - raise Exception('Error reading svg file {}.'.format(svg_file)) from exc - else: - return svg + raise ValueError(f"Error reading svg file {svg_file}.") from exc if isinstance(svg_file, sg.SVGFigure): return svg_file - raise ValueError('Expected `svg_file` to be `str` or `svgutils.SVG`, got {}.'.format(type(svg_file))) + raise ValueError( + f"Expected `svg_file` to be `str` or `svgutils.SVG`, got {type(svg_file)}." + ) -def merge_svg_files(svg_file1, svg_file2, x_coord, y_coord, scale=1): - """ Merge `svg_file2` in `svg_file1` in the given positions `x_coord`, `y_coord` and `scale`. +def merge_svg_files( + svg_file1: str | svgutils.SVGFigure, + svg_file2: str | svgutils.SVGFigure, + x_coord: float, + y_coord: float, + scale: float = 1, +) -> svgutils.SVGFigure: + """Merge `svg_file2` in `svg_file1` in the given positions `x_coord`, `y_coord` and `scale`. Parameters ---------- @@ -94,51 +102,69 @@ def merge_svg_files(svg_file1, svg_file2, x_coord, y_coord, scale=1): ------- `svg1` svgutils object with the content of 'svg_file2' """ - svg1 = _check_svg_file(svg_file1) - svg2 = _check_svg_file(svg_file2) + svg1 = _check_svg_file(svg_file=svg_file1) + svg2 = _check_svg_file(svg_file=svg_file2) svg2_root = svg2.getroot() svg1.append([svg2_root]) - svg2_root.moveto(x_coord, y_coord, scale=scale) return svg1 -def rsvg_export(input_file, output_file, dpi=90, rsvg_binpath=None): - """ Calls the `rsvg-convert` command, to convert a svg to a PDF (with unicode). +def rsvg_export( + input_file: os.PathLike | str, + output_file: os.PathLike | str, + dpi: int = 90, + rsvg_binpath: os.PathLike | str | None = None, +): + """Calls the `rsvg-convert` command, to convert a svg to a PDF (with unicode). Parameters ---------- - rsvg_binpath: str - Path to `rsvg-convert` command - - input_file: str + input_file: os.PathLike | str Path to the input file - output_file: str + output_file: os.PathLike | str Path to the output file + dpi: int + Dots per inch for the output file. Default is 90. + + rsvg_binpath: os.PathLike | str | None + Path to `rsvg-convert` command binary. + If None, it will try to find the binary in your computer. + + Raises + ------ + FileNotFoundError + If the input file does not exist or if the `rsvg-convert` command is not found. + + CalledProcessError + If the `rsvg-convert` command fails to execute properly. + Returns ------- return_value Command call return value - """ - if not os.path.exists(input_file): - log.error('File {} not found.'.format(input_file)) - raise IOError((0, 'File not found.', input_file)) + _input_file = Path(input_file) + if not _input_file.exists(): + raise FileNotFoundError(f"File {input_file} not found.") if rsvg_binpath is None: - rsvg_binpath = which('rsvg-convert') - check_command(rsvg_binpath) - - args_strings = [] - args_strings += ["-f pdf"] - args_strings += ["-o {}".format(output_file)] - args_strings += ["--dpi-x {}".format(dpi)] - args_strings += ["--dpi-y {}".format(dpi)] - args_strings += [input_file] + cmd_name = "rsvg-convert" + rsvg_binpath = shutil.which(cmd="rsvg-convert") + if rsvg_binpath is None: + raise FileNotFoundError(f"Could not find command named {cmd_name}.") + + args_strings = [ + "-f pdf", + f"-o {output_file}", + f"--dpi-x {dpi}", + f"--dpi-y {dpi}", + str(input_file), + ] - return call_command(rsvg_binpath, args_strings) + return call_command(cmd_name=rsvg_binpath, args_strings=args_strings) diff --git a/docstamp/template.py b/docstamp/template.py index f4314ea..7a28274 100644 --- a/docstamp/template.py +++ b/docstamp/template.py @@ -1,128 +1,88 @@ -# coding=utf-8 -# ------------------------------------------------------------------------------- -# Author: Alexandre Manhaes Savio -# Grupo de Inteligencia Computational -# Universidad del Pais Vasco UPV/EHU -# -# 2015, Alexandre Manhaes Savio -# Use this at your own risk! -# ------------------------------------------------------------------------------- +"""A module for handling document templates and rendering them.""" +from __future__ import annotations import os import shutil -import logging +from pathlib import Path +from typing import TYPE_CHECKING from jinja2 import Environment, FileSystemLoader +from .exceptions import ExportError, RenderingError +from .file_utils import get_tempfile from .inkscape import svg2pdf, svg2png from .pdflatex import tex2pdf, xetex2pdf -from .file_utils import get_tempfile, write_to_file from .svg_utils import replace_chars_for_svg_code -log = logging.getLogger(__name__) +if TYPE_CHECKING: + from typing import Any, Literal -def get_environment_for(file_path): - """Return a Jinja2 environment for where file_path is. - - Parameters - ---------- - file_path: str - - Returns - ------- - jinja_env: Jinja2.Environment - - """ - work_dir = os.path.dirname(os.path.abspath(file_path)) - - if not os.path.exists(work_dir): - raise IOError('Could not find folder for dirname of file {}.'.format(file_path)) - - try: - jinja_env = Environment(loader=FileSystemLoader(work_dir)) - except: - raise - else: - return jinja_env - - -def get_doctype_by_extension(extension): - if 'txt' in extension: +def get_doctype_by_extension( + extension: Literal["txt", "svg", "tex"] | str, +) -> type[TextDocument] | None: + if "txt" in extension: doc_type = TextDocument - elif 'svg' in extension: + elif "svg" in extension: doc_type = SVGDocument - elif 'tex' in extension: + elif "tex" in extension: doc_type = LateXDocument else: - raise ValueError('Could not identify the `doc_type` for `extension` {}.'.format(extension)) - + doc_type = None return doc_type -def get_doctype_by_command(command): +def get_doctype_by_command( + command: Literal["inkscape", "pdflatex", "xelatex"] | None, +) -> type[TextDocument] | None: if not command: doc_type = TextDocument - elif command == 'inkscape': + elif command == "inkscape": doc_type = SVGDocument - elif command == 'pdflatex': + elif command == "pdflatex": doc_type = PDFLateXDocument - elif command == 'xelatex': + elif command == "xelatex": doc_type = XeLateXDocument else: - raise ValueError('Could not identify the `doc_type` for `command` {}.'.format(command)) + doc_type = None # type: ignore[unreachable] return doc_type -class TextDocument(object): - """ A plain text document model. +class TextDocument: + """A plain text document model. Parameters ---------- template_file_path: str Document template file path. - - doc_contents: dict - Dictionary with content values for the template to be filled. """ - def __init__(self, template_file_path, doc_contents=None): - if not os.path.exists(template_file_path): - raise IOError('Could not find template file {}.'.format(template_file_path)) - - self._setup_template_file(template_file_path) + def __init__( + self, + template_file_path: os.PathLike | str, + ): + self._template_file = Path(template_file_path) - if doc_contents is not None: - self.file_content_ = self.fill(doc_contents) + if not self._template_file.exists(): + raise FileNotFoundError( + f"Could not find template file {template_file_path}." + ) - def _setup_template_file(self, template_file_path): - """ Setup self.template + self._template_env = Environment( + loader=FileSystemLoader(self._template_file.parent), + autoescape=False, # noqa: S701 + ) + self.template = self._template_env.get_template(self._template_file.name) - Parameters - ---------- - template_file_path: str - Document template file path. - """ - try: - template_file = template_file_path - template_env = get_environment_for(template_file_path) - template = template_env.get_template(os.path.basename(template_file)) - except: - raise - else: - self._template_file = template_file - self._template_env = template_env - self.template = template - - def fill(self, doc_contents): - """ Fill the content of the document with the information in doc_contents. + def render(self, doc_contents: dict[str, Any] | None = None) -> str: + """Render the content of the document with the information in doc_contents. Parameters ---------- - doc_contents: dict - Set of values to set the template document. + doc_contents: dict[str, Any] + Dictionary with content values for the template to be filled. Returns ------- @@ -130,45 +90,47 @@ def fill(self, doc_contents): The content of the document with the template information filled. """ try: - filled_doc = self.template.render(**doc_contents) - except: - log.exception('Error rendering Document ' - 'for {}.'.format(doc_contents)) - raise - else: - self.file_content_ = filled_doc - return filled_doc - - def save_content(self, file_path, encoding='utf-8'): - """ Save the content of the .txt file in a text file. + return self.template.render(**doc_contents) + except Exception as error: + raise RenderingError( + f"Error rendering document for {doc_contents}." + ) from error + + def export( + self, + file_path: os.PathLike | str, + doc_contents: dict[str, Any], + **kwargs: Any, + ): + """Export the rendered document to a file. Parameters ---------- - file_path: str + file_path: Path Path to the output file. + doc_contents: dict[str, Any] + Dictionary with content values for the template to be filled. + encoding: str + Encoding to use when writing the file. Default is 'utf-8'. """ - if self.file_content_ is None: - msg = 'Template content has not been updated. \ - Please fill the template before rendering it.' - log.exception(msg) - raise ValueError(msg) - + rendered_content = self.render(doc_contents=doc_contents) + _file_path = Path(file_path) + encoding = kwargs.get("encoding", "utf-8") try: - write_to_file(file_path, content=self.file_content_, - encoding=encoding) - except Exception as exc: - msg = 'Document of type {} got an error when \ - writing content.'.format(self.__class__) - log.exception(msg) - raise Exception(msg) from exc - - def render(self, file_path, **kwargs): - """ See self.save_content """ - return self.save_content(file_path) + _file_path.write_text( + rendered_content, + encoding=encoding, + ) + except Exception as error: + raise ExportError(f"Error exporting document to {file_path}.") from error @classmethod - def from_template_file(cls, template_file_path, command=None): - """ Factory function to create a specific document of the + def from_template_file( + cls, + template_file_path: Path, + command: Literal["inkscape", "pdflatex", "xelatex"] | None = None, + ) -> TextDocument: + """Factory function to create a specific document of the class given by the `command` or the extension of `template_file_path`. See get_doctype_by_command and get_doctype_by_extension. @@ -185,24 +147,26 @@ class given by the `command` or the extension of `template_file_path`. """ # get template file extension - ext = os.path.basename(template_file_path).split('.')[-1] + ext = template_file_path.suffix.lower().removeprefix(".") - try: - doc_type = get_doctype_by_command(command) - except ValueError: + doc_type = get_doctype_by_command(command) + if doc_type is None: doc_type = get_doctype_by_extension(ext) - except: - raise - else: - return doc_type(template_file_path) + + if doc_type is None: + raise ValueError( + f"Unsupported document type for file {template_file_path}. " + "Supported types are: txt, svg, tex." + ) + + return doc_type(template_file_path) class SVGDocument(TextDocument): - """ A .svg template document model. See GenericDocument. """ - _template_file = 'badge_template.svg' + """A .svg template document model. See TextDocument.""" - def fill(self, doc_contents): - """ Fill the content of the document with the information in doc_contents. + def render(self, doc_contents: dict[str, Any] | None = None) -> str: + """Render the content of the document with the information in doc_contents. This is different from the TextDocument fill function, because this will check for symbools in the values of `doc_content` and replace them to good XML codes before filling the template. @@ -217,21 +181,40 @@ def fill(self, doc_contents): filled_doc: str The content of the document with the template information filled. """ + if doc_contents is None: + doc_contents = {} + for key, content in doc_contents.items(): doc_contents[key] = replace_chars_for_svg_code(content) - return super(SVGDocument, self).fill(doc_contents=doc_contents) - - def render(self, file_path, **kwargs): - """ Save the content of the .svg file in the chosen rendered format. + try: + return super().render(doc_contents=doc_contents) + except Exception as error: + raise RenderingError( + f"Error rendering SVG document for {doc_contents}." + ) from error + + def export( + self, + file_path: os.PathLike | str, + doc_contents: dict[str, Any], + **kwargs, + ): + """Export the content of the .svg file in the chosen rendered format. Parameters ---------- file_path: str Path to the output file. + doc_contents: dict[str, Any] + Dictionary with content values for the template to be filled. + Kwargs ------ + encoding: str + Encoding to use when writing the file. Default is 'utf-8'. + file_type: str Choices: 'png', 'pdf', 'svg' Default: 'pdf' @@ -242,49 +225,82 @@ def render(self, file_path, **kwargs): support_unicode: bool Whether to allow unicode to be encoded in the PDF. - Default: False + Default: True """ - temp = get_tempfile(suffix='.svg') - self.save_content(temp.name) - - file_type = kwargs.get('file_type', 'pdf') - dpi = kwargs.get('dpi', 150) - support_unicode = kwargs.get('support_unicode', False) + temp = get_tempfile(suffix=".svg") + rendered_content = self.render(doc_contents=doc_contents) + _file_path = Path(file_path) try: - if file_type == 'svg': - shutil.copyfile(temp.name, file_path) - elif file_type == 'png': - svg2png(temp.name, file_path, dpi=dpi) - elif file_type == 'pdf': - svg2pdf(temp.name, file_path, dpi=dpi, support_unicode=support_unicode) - except: - log.exception( - 'Error exporting file {} to {}'.format(file_path, file_type) + _file_path.write_text( + rendered_content, + encoding=kwargs.get("encoding", "utf-8"), ) - raise + except Exception as error: + raise ExportError( + f"Error exporting SVG document to {file_path}." + ) from error + + file_type = kwargs.get("file_type", "pdf") + dpi = kwargs.get("dpi", 150) + support_unicode = kwargs.get("support_unicode", True) + try: + if file_type == "svg": + shutil.copyfile(src=temp.name, dst=file_path) + elif file_type == "png": + svg2png(svg_file_path=temp.name, png_file_path=file_path, dpi=dpi) + elif file_type == "pdf": + svg2pdf( + svg_file_path=temp.name, + pdf_file_path=file_path, + dpi=dpi, + support_unicode=support_unicode, + ) + except Exception as e: + raise RenderingError( + f"Error exporting file {file_path} to {file_type}." + ) from e class LateXDocument(TextDocument): - """ A .tex template document model. See GenericDocument. """ + """A .tex template document model. See GenericDocument.""" - _render_function = staticmethod(tex2pdf) + _export = staticmethod(tex2pdf) - def render(self, file_path, **kwargs): - """ Save the content of the .text file in the PDF. + def export( + self, file_path: os.PathLike | str, doc_contents: dict[str, Any], **kwargs + ): + """Export the content of the .tex file in the PDF. Parameters ---------- file_path: str Path to the output file. + + doc_contents: dict[str, Any] + Dictionary with content values for the template to be filled. + + Kwargs + ------ + encoding: str + Encoding to use when writing the file. Default is 'utf-8'. """ - temp = get_tempfile(suffix='.tex') - self.save_content(temp.name) + temp = get_tempfile(suffix=".tex") + rendered_content = self.render(doc_contents=doc_contents) + _file_path = Path(file_path) + try: + _file_path.write_text( + rendered_content, + encoding=kwargs.get("encoding", "utf-8"), + ) + except Exception as error: + raise ExportError( + f"Error exporting TeX document to {_file_path}." + ) from error try: - self._render_function(temp.name, file_path, output_format='pdf') - except: - log.exception('Error exporting file {} to PDF.'.format(file_path)) - raise + self._export(temp.name, _file_path, output_format="pdf") + except Exception as error: + raise ExportError(f"Error exporting file {_file_path} to PDF.") from error class PDFLateXDocument(LateXDocument): @@ -292,4 +308,4 @@ class PDFLateXDocument(LateXDocument): class XeLateXDocument(LateXDocument): - _render_function = staticmethod(xetex2pdf) + _export = staticmethod(xetex2pdf) diff --git a/docstamp/unicode_csv.py b/docstamp/unicode_csv.py deleted file mode 100644 index 2c1cab0..0000000 --- a/docstamp/unicode_csv.py +++ /dev/null @@ -1,39 +0,0 @@ -# -*- coding: utf-8 -*- - -import csv -import codecs - -try: - from cStringIO import StringIO -except: - from io import StringIO - - -class UnicodeWriter: - """ - A CSV writer which will write rows to CSV file "f", - which is encoded in the given encoding. - """ - - def __init__(self, f, dialect=csv.excel, encoding="utf-8", **kwds): - # Redirect output to a queue - self.queue = StringIO() - self.writer = csv.writer(self.queue, dialect=dialect, **kwds) - self.stream = f - self.encoder = codecs.getincrementalencoder(encoding)() - - def writerow(self, row): - self.writer.writerow([s.encode("utf-8") for s in row]) - # Fetch UTF-8 output from the queue ... - data = self.queue.getvalue() - data = data.decode("utf-8") - # ... and reencode it into the target encoding - data = self.encoder.encode(data) - # write to the target stream - self.stream.write(data) - # empty queue - self.queue.truncate(0) - - def writerows(self, rows): - for row in rows: - self.writerow(row) diff --git a/docstamp/vcard.py b/docstamp/vcard.py index 0bdd310..0c0844d 100644 --- a/docstamp/vcard.py +++ b/docstamp/vcard.py @@ -2,39 +2,57 @@ Function helpers to create vcard formats. """ +from __future__ import annotations -def create_vcard3_str(name, surname, displayname, email='', org='', title='', url='', note=''): - """ Create a vCard3.0 string with the given parameters. +from dataclasses import dataclass + + +@dataclass +class VCard3Data: + """Data container for vCard3.0 data structure.""" + + name: str + surname: str + displayname: str + email: str = "" + org: str = "" + title: str = "" + url: str = "" + note: str = "" + + +def create_vcard3_str(data: VCard3Data) -> str: + """Create a vCard3.0 string with the given parameters. Reference: http://www.evenx.com/vcard-3-0-format-specification """ vcard = [] - vcard += ['BEGIN:VCARD'] - vcard += ['VERSION:3.0'] + vcard += ["BEGIN:VCARD"] + vcard += ["VERSION:3.0"] - if name and surname: - name = name.strip() - vcard += ['N:{};{};;;'.format(name, surname)] + if data.name and data.surname: + name = data.name.strip() + vcard += [f"N:{name};{data.surname};;;"] - if not displayname: - displayname = '{} {}'.format(name, surname) + if not data.displayname: + displayname = f"{name} {data.surname}" - vcard += ['FN:{}'.format(displayname)] + vcard += [f"FN:{displayname}"] - if email: - vcard += ['EMAIL:{}'.format(email)] + if data.email: + vcard += [f"EMAIL:{data.email}"] - if org: - vcard += ['ORG:{}'.format(org)] + if data.org: + vcard += [f"ORG:{data.org}"] - if title: - vcard += ['TITLE:{}'.format(title)] + if data.title: + vcard += [f"TITLE:{data.title}"] - if url: - vcard += ['URL:{}'.format(url)] + if data.url: + vcard += [f"URL:{data.url}"] - if note: - vcard += ['NOTE:{}'.format(note)] + if data.note: + vcard += [f"NOTE:{data.note}"] - vcard += ['END:VCARD'] + vcard += ["END:VCARD"] - return '\n'.join([field.strip() for field in vcard]) + return "\n".join([field.strip() for field in vcard]) diff --git a/docstamp/version.py b/docstamp/version.py index 9a08b71..da793c1 100644 --- a/docstamp/version.py +++ b/docstamp/version.py @@ -1,2 +1 @@ -"""Release version number.""" -__version__ = '0.4.5' # noqa +__version__ = "0.4.4.post9.dev0+f77fcf8" diff --git a/docstamp/xml_utils.py b/docstamp/xml_utils.py index 3e62f4a..87e8d48 100644 --- a/docstamp/xml_utils.py +++ b/docstamp/xml_utils.py @@ -2,6 +2,9 @@ Function helpers to treat XML content. """ +from __future__ import annotations + +import os from xml.sax.saxutils import escape, unescape from .file_utils import replace_file_content @@ -17,9 +20,10 @@ xml_unescape_table = {v: k for k, v in xml_escape_table.items()} -def xml_escape(text): - """ Replace not valid characters for XML such as &, < and > to - their valid replacement strings +def xml_escape(text: str) -> str: + """ + Replace not valid characters for XML such as &, < and > to + their valid replacement strings Parameters ---------- @@ -33,8 +37,8 @@ def xml_escape(text): return escape(text, xml_escape_table) -def xml_unescape(text): - """ Do the inverse of `xml_escape`. +def xml_unescape(text: str) -> str: + """Do the inverse of `xml_escape`. Parameters ---------- @@ -48,8 +52,10 @@ def xml_unescape(text): return unescape(text, xml_unescape_table) -def change_xml_encoding(filepath, src_enc, dst_enc='utf-8'): - """ Modify the encoding entry in the XML file. +def change_xml_encoding( + filepath: os.PathLike | str, src_enc: str, dst_enc: str = "utf-8" +): + """Modify the encoding entry in the XML file. Parameters ---------- @@ -63,4 +69,9 @@ def change_xml_encoding(filepath, src_enc, dst_enc='utf-8'): Encoding to be set in the file. """ enc_attr = "encoding='{}'" - replace_file_content(filepath, enc_attr.format(src_enc), enc_attr.format(dst_enc), 1) + replace_file_content( + filepath=filepath, + old=enc_attr.format(src_enc), + new=enc_attr.format(dst_enc), + max=1, + ) diff --git a/justfile b/justfile new file mode 100644 index 0000000..4175a9a --- /dev/null +++ b/justfile @@ -0,0 +1,89 @@ +project-name := "docstamp" + +# Show available recipes +default: + @just --list + +# Show the version of the project +version: + hatch version + +# Install the dependencies necessary for CI and development +install: + uv sync + +# Install the dependencies needed for a production installation +install-prod: + uv sync --no-default-groups + +# Upgrade the dependencies to the latest accepted versions +upgrade: + uv lock --upgrade + +# Delete all intermediate files +clean-temp: clean-build clean-pyc + +# Delete all intermediate files and caches +clean-all: clean-temp clean-caches + +# Delete the Python build files and folders +clean-build: + rm -fr build/ + rm -fr dist/ + rm -fr .eggs/ + rm -fr *.egg-info + rm -fr *.spec + +# Remove Python file artifacts +clean-pyc: + pyclean {{project-name}} + find . -name '*~' -exec rm -f {} + + find . -name __pycache__ -exec rm -rf {} + + find . -name '*.log*' -delete + find . -name '*_cache' -exec rm -rf {} + + find . -name '*.egg-info' -exec rm -rf {} + + +# Remove cache directories +clean-caches: + rm -rf .mypy_cache + rm -rf .ruff_cache + rm -rf .pytest_cache + +# Remove all build, Python, and cache artifacts +clean: clean-build clean-pyc clean-caches + +##@ Code check +# Format your code with ruff +format-ruff: + ruff format . + +# Format your code +format: format-ruff + +# Run mypy check +lint-mypy: + mypy . + +# Run ruff lint check +lint-ruff: + ruff check --fix . + +# Run all code checks +lint: lint-mypy lint-ruff + +# Run tests with coverage +test args="": + pytest --cov -vvv {{args}} + +# Run tests in debug mode +test-dbg args="": + pytest --pdb --ff {{args}} + +# Run format, linting, then tests +check: format lint test + +build: + uv build + +release: + uv release \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..efd370c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,165 @@ +[project] +name = "docstamp" +dynamic = ["version"] +requires-python = ">= 3.12" +dependencies = [ + "Pillow>=6.1.0", + "jinja2>=2.10", + "PyPDF2>=1.26.0", + "qrcode>=6.1", + "svgutils==0.3.1", + "click>=7.0", + "orjson>=3.10.18", +] +authors = [ + {name = "Alexandre Manhaes Savio", email = "alexsavio@github.com"}, +] +description = "A SVG and LateX template renderer from table data based on Inkscape and Jinja2" +readme = "README.md" +license = "Apache-2.0" +license-files = ["LICENSE"] +keywords = [ + "svg", + "latex", + "template", + "renderer", + "inkscape", + "jinja2", + "pdf", + "qrcode", + "cli", + "render", + "badge", + "inkscape", + "pdf", +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Natural Language :: English", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] + +[project.urls] +Homepage = "https://github.com/PythonSanSebastian/docstamp" +Documentation = "https://github.com/PythonSanSebastian/docstamp/blob/master/README.md" +Repository = "https://github.com/PythonSanSebastian/docstamp" +Issues = "https://github.com/PythonSanSebastian/docstamp/issues" +Changelog = "https://github.com/PythonSanSebastian/docstamp/blob/master/CHANGES.rst" + +[build-system] +requires = ["hatchling", "uv-dynamic-versioning"] +build-backend = "hatchling.build" + +[tool.hatch.version] +source = "uv-dynamic-versioning" + +[tool.hatch.build.hooks.version] +path = "docstamp/version.py" +template = ''' +__version__ = "{version}" +''' + +[project.scripts] +docstamp = "docstamp.cli.main:cli" + +[tool.uv] +default-groups = "all" + +[dependency-groups] +dev = [ + "hatch", + "mypy", + "ruff", + "bumpversion", +] + +[tool.mypy] +python_version = "3.12" +implicit_reexport = false +ignore_missing_imports = true +no_implicit_optional = true +warn_unused_configs = true +warn_unused_ignores = true +warn_redundant_casts = true +warn_unreachable = true +warn_no_return = true +exclude = '''(?x)( +^build/ +|^dist/ +|^.venv/ +) +''' + +[tool.ruff] +target-version = "py312" +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "build", + "dist", + "venv", +] + +# list of all rules: https://beta.ruff.rs/docs/rules/ +lint.ignore = [ + "E501", # line-too-long: we're fine with what black gives us + "F722", # forward-annotation-syntax-error: https://github.com/PyCQA/pyflakes/issues/542 + "PTH123", # pathlib-open: It's ok to use open(...) instead of Pathlib(...).open() + "UP007", # typing-union: pydantic & strawberry don't handle these well in Python 3.9 + "SIM108", # if-else-block-instead-of-if-exp: ternary operator isn't always preferred + "RUF012", # ruff - mutable class attributes should be annotated with `typing.ClassVar` + "B008", # flake8-bugbear - Do not perform function call `Parameter` in argument defaultsRuff(B008) + "S108", # Probable insecure usage of temporary file or directory: "/tmp +] +lint.select = [ + "E", + "F", + "W", + "G", + "PT", + "ERA", + "B", + "C90", + "YTT", + "S", + "A001", + "C4", + "T10", + "ICN", + "INP", + "PIE", + "T20", + "SIM", + "PTH", + "PGH", + "PL", + "RUF", + "I", + "UP", +] + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = [ + "E402", # module-import-not-at-top-of-file: init files occasionally need imports at the bottom +] diff --git a/scripts/embed_font_to_svg.py b/scripts/embed_font_to_svg.py index ad03f8e..7601a1f 100755 --- a/scripts/embed_font_to_svg.py +++ b/scripts/embed_font_to_svg.py @@ -1,48 +1,64 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -import os -import base64 +from __future__ import annotations + import argparse +import base64 import logging -from lxml import etree +import os +import sys +from lxml import etree -FONT_TYPES = {'ttf': 'truetype', - 'otf': 'opentype'} +FONT_TYPES = {"ttf": "truetype", "otf": "opentype"} def create_argparser(): - parser = argparse.ArgumentParser(description='Embed base64 font to SVG file') - parser.add_argument('-i', '--input', action='store', dest='svg_filepath', - default='', - help='The SVG file path. If you dont give input file, ' - ' will output to the file or stdout') - parser.add_argument('-f', '--font', action='append', dest='fonts', - default=[], - help='Font file. You can add as many as you want.') - parser.add_argument('-o', '--output', action='store', dest='out_path', - default='', - help='The resulting SVG file path. Overwritten if exist.') + parser = argparse.ArgumentParser(description="Embed base64 font to SVG file") + parser.add_argument( + "-i", + "--input", + action="store", + dest="svg_filepath", + default="", + help="The SVG file path. If you dont give input file, " + " will output to the file or stdout", + ) + parser.add_argument( + "-f", + "--font", + action="append", + dest="fonts", + default=[], + help="Font file. You can add as many as you want.", + ) + parser.add_argument( + "-o", + "--output", + action="store", + dest="out_path", + default="", + help="The resulting SVG file path. Overwritten if exist.", + ) return parser def get_base64_encoding(bin_filepath): """Return the base64 encoding of the given binary file""" - return base64.b64encode(open(bin_filepath, 'r').read()) + return base64.b64encode(open(bin_filepath).read()) def remove_ext(filepath): """Return the basename of filepath without extension.""" - return os.path.basename(filepath).split('.')[0] + return os.path.basename(filepath).split(".")[0] def get_ext(filepath): """Return file extension""" - return os.path.basename(filepath).split('.')[-1] + return os.path.basename(filepath).split(".")[-1] -class FontFace(object): +class FontFace: """CSS font-face object""" def __init__(self, filepath, fonttype=None, name=None): @@ -78,15 +94,15 @@ def ext(self): @property def css_text(self): - css_text = u"@font-face{\n" - css_text += u"font-family: " + self.name + ";\n" - css_text += u"src: url(data:font/" + self.ext + ";" - css_text += u"base64," + self.base64 + ") " - css_text += u"format('" + self.fonttype + "');\n}\n" + css_text = "@font-face{\n" + css_text += "font-family: " + self.name + ";\n" + css_text += "src: url(data:font/" + self.ext + ";" + css_text += "base64," + self.base64 + ") " + css_text += "format('" + self.fonttype + "');\n}\n" return css_text -class FontFaceGroup(object): +class FontFaceGroup: """Group of FontFaces""" def __init__(self): @@ -94,10 +110,10 @@ def __init__(self): @property def css_text(self): - css_text = u'' + css_text += "" return css_text @property @@ -108,8 +124,7 @@ def append(self, font_face): self.fontfaces.append(font_face) -if __name__ == '__main__': - +if __name__ == "__main__": logging.basicConfig(level=logging.INFO) log = logging.getLogger(__file__) @@ -117,9 +132,9 @@ def append(self, font_face): try: args = parser.parse_args() except argparse.ArgumentError as exc: - log.exception('Error parsing arguments.') + log.exception("Error parsing arguments.") parser.error(str(exc.message)) - exit(-1) + sys.exit(-1) svg_filepath = args.svg_filepath fonts = args.fonts @@ -131,8 +146,8 @@ def append(self, font_face): if not svg_filepath: raw_write = True elif not os.path.exists(svg_filepath): - log.error('Could not find file: {}'.format(svg_filepath)) - exit(-1) + log.error(f"Could not find file: {svg_filepath}") + sys.exit(-1) if not out_path: raw_write = True @@ -140,8 +155,8 @@ def append(self, font_face): # check if user gave any font if not fonts: - log.error('No fonts given.') - exit(-1) + log.error("No fonts given.") + sys.exit(-1) # build the stuff to write fontfaces = FontFaceGroup() @@ -151,20 +166,20 @@ def append(self, font_face): # write the stuff if raw_write and stdout: print(fontfaces.css_text) - exit(0) + sys.exit(0) elif raw_write: xtree = etree.ElementTree(fontfaces.xml_elem) xtree.write(out_path) - exit(0) + sys.exit(0) else: - with open(svg_filepath, 'r') as svgf: + with open(svg_filepath) as svgf: tree = etree.parse(svgf) for element in tree.iter(): - if element.tag.split("}")[1] == 'svg': + if element.tag.split("}")[1] == "svg": break element.insert(0, fontfaces.xml_elem) - tree.write(out_path, encoding='utf-8', pretty_print=True) - exit(0) + tree.write(out_path, encoding="utf-8", pretty_print=True) + sys.exit(0) diff --git a/scripts/svg_export.py b/scripts/svg_export.py index 1955a77..8013df0 100755 --- a/scripts/svg_export.py +++ b/scripts/svg_export.py @@ -1,8 +1,10 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -import logging +from __future__ import annotations + import argparse +import logging +import sys from docstamp.inkscape import svg2pdf, svg2png @@ -12,35 +14,48 @@ def create_argparser(): parser = argparse.ArgumentParser() - parser.add_argument('-i', '--input', action='store', dest='input', - help='Input .svg file path') - parser.add_argument('-o', '--output', action='store', dest='output', - help='Output file path') - parser.add_argument('-t', '--type', choices=['pdf', 'png'], - action='store', dest='file_type', default='pdf', - help='Output file type') - parser.add_argument('--dpi', type=int, action='store', dest='dpi', - default=150, help='Output file resolution') + parser.add_argument( + "-i", "--input", action="store", dest="input", help="Input .svg file path" + ) + parser.add_argument( + "-o", "--output", action="store", dest="output", help="Output file path" + ) + parser.add_argument( + "-t", + "--type", + choices=["pdf", "png"], + action="store", + dest="file_type", + default="pdf", + help="Output file type", + ) + parser.add_argument( + "--dpi", + type=int, + action="store", + dest="dpi", + default=150, + help="Output file resolution", + ) return parser -if __name__ == '__main__': - +if __name__ == "__main__": parser = create_argparser() try: args = parser.parse_args() except argparse.ArgumentError as exc: - log.exception('Error parsing arguments.') + log.exception("Error parsing arguments.") parser.error(str(exc.message)) - exit(-1) + sys.exit(-1) input_file = args.input output_file = args.output file_type = args.file_type dpi = args.dpi - if file_type == 'png': + if file_type == "png": svg2png(input_file, output_file, dpi=dpi) - elif file_type == 'pdf': + elif file_type == "pdf": svg2pdf(input_file, output_file, dpi=dpi) diff --git a/setup.cfg b/setup.cfg index a65ca40..f66cc0a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,13 +4,15 @@ commit = True tag = False message = :bookmark: Bump version: {current_version} → {new_version} + + [metadata] name = docstamp version = attr: docstamp.version.__version__ description = A SVG and LateX template renderer from table data based on Inkscape and Jinja2 long_description = file: README.md long_description_content_type = text/markdown -project_urls = +project_urls = Documentation = https://github.com/PythonSanSebastian/docstamp/blob/master/README.md Source Code = https://github.com/PythonSanSebastian/docstamp Bug Tracker = https://github.com/PythonSanSebastian/docstamp/issues @@ -19,7 +21,7 @@ author = Alexandre M. Savio author_email = info@pyss.org license = Apache License Version 2.0 license-file = LICENSE -keywords = +keywords = svg latex template @@ -28,7 +30,7 @@ keywords = badge document render -classifiers = +classifiers = Development Status :: 4 - Beta Environment :: Other Environment Intended Audience :: Developers @@ -41,22 +43,22 @@ classifiers = python_requires = >=3.6 packages = find: include_package_data = True -setup_requires = +setup_requires = wheel setuptools -install_requires = +install_requires = Pillow>=6.1.0 jinja2>=2.10 PyPDF2>=1.26.0 qrcode>=6.1 svgutils==0.3.1 click>=7.0 -scripts = +scripts = scripts/svg_export.py scripts/embed_font_to_svg.py [options.entry_points] -console_scripts = +console_scripts = docstamp = docstamp.cli.cli:cli [flake8] @@ -85,7 +87,7 @@ ignore_missing_imports = True warn_unused_configs = True [tox:tox] -envlist = +envlist = lint, isort, mypy, @@ -94,17 +96,17 @@ skipsdist = True [testenv] basepython = python3 whitelist_externals = make -deps = +deps = lint: flake8 lint: flake8-bugbear isort: isort mypy: mypy -passenv = +passenv = CI = 1 -setenv = +setenv = PYTHONPATH = {toxinidir}:{toxinidir} TESTING = True -commands = +commands = lint: flake8 docstamp isort: isort -c -rc docstamp mypy: mypy docstamp diff --git a/setup.py b/setup.py deleted file mode 100644 index 85a38ac..0000000 --- a/setup.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Define the setup function using setup.cfg.""" - -from setuptools import setup - -setup() diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..b81702c --- /dev/null +++ b/uv.lock @@ -0,0 +1,899 @@ +version = 1 +revision = 2 +requires-python = ">=3.12" + +[[package]] +name = "anyio" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, +] + +[[package]] +name = "bump2version" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/2a/688aca6eeebfe8941235be53f4da780c6edee05dbbea5d7abaa3aab6fad2/bump2version-1.0.1.tar.gz", hash = "sha256:762cb2bfad61f4ec8e2bdf452c7c267416f8c70dd9ecb1653fd0bbb01fa936e6", size = 36236, upload-time = "2020-10-07T18:38:40.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/e3/fa60c47d7c344533142eb3af0b73234ef8ea3fb2da742ab976b947e717df/bump2version-1.0.1-py2.py3-none-any.whl", hash = "sha256:37f927ea17cde7ae2d7baf832f8e80ce3777624554a653006c9144f8017fe410", size = 22030, upload-time = "2020-10-07T18:38:38.148Z" }, +] + +[[package]] +name = "bumpversion" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bump2version" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/f5/e95fcd8de146884cf5ecf30f227e13c3615584ccef8c8cca18140a27b664/bumpversion-0.6.0.tar.gz", hash = "sha256:4ba55e4080d373f80177b4dabef146c07ce73c7d1377aabf9d3c3ae1f94584a6", size = 11897, upload-time = "2020-05-14T02:19:39.534Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/ff/93f0db7b3ca337e9f2a289980083e858775dfb3672b38052c6911b36ea66/bumpversion-0.6.0-py2.py3-none-any.whl", hash = "sha256:4eb3267a38194d09f048a2179980bb4803701969bff2c85fa8f6d1ce050be15e", size = 8449, upload-time = "2020-05-14T02:19:37.745Z" }, +] + +[[package]] +name = "certifi" +version = "2025.4.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" }, +] + +[[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, upload-time = "2024-09-04T20:45:21.852Z" } +wheels = [ + { 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, upload-time = "2024-09-04T20:44:12.232Z" }, + { 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, upload-time = "2024-09-04T20:44:13.739Z" }, + { 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, upload-time = "2024-09-04T20:44:15.231Z" }, + { 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, upload-time = "2024-09-04T20:44:17.188Z" }, + { 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, upload-time = "2024-09-04T20:44:18.688Z" }, + { 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, upload-time = "2024-09-04T20:44:20.248Z" }, + { 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, upload-time = "2024-09-04T20:44:21.673Z" }, + { 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, upload-time = "2024-09-04T20:44:23.245Z" }, + { 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, upload-time = "2024-09-04T20:44:24.757Z" }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "cryptography" +version = "45.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/1f/9fa001e74a1993a9cadd2333bb889e50c66327b8594ac538ab8a04f915b7/cryptography-45.0.3.tar.gz", hash = "sha256:ec21313dd335c51d7877baf2972569f40a4291b76a0ce51391523ae358d05899", size = 744738, upload-time = "2025-05-25T14:17:24.777Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/3d/ac361649a0bfffc105e2298b720d8b862330a767dab27c06adc2ddbef96a/cryptography-45.0.3-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d377dde61c5d67eb4311eace661c3efda46c62113ff56bf05e2d679e02aebb5b", size = 4205541, upload-time = "2025-05-25T14:16:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/70/3e/c02a043750494d5c445f769e9c9f67e550d65060e0bfce52d91c1362693d/cryptography-45.0.3-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fae1e637f527750811588e4582988932c222f8251f7b7ea93739acb624e1487f", size = 4433275, upload-time = "2025-05-25T14:16:16.421Z" }, + { url = "https://files.pythonhosted.org/packages/40/7a/9af0bfd48784e80eef3eb6fd6fde96fe706b4fc156751ce1b2b965dada70/cryptography-45.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ca932e11218bcc9ef812aa497cdf669484870ecbcf2d99b765d6c27a86000942", size = 4209173, upload-time = "2025-05-25T14:16:18.163Z" }, + { url = "https://files.pythonhosted.org/packages/31/5f/d6f8753c8708912df52e67969e80ef70b8e8897306cd9eb8b98201f8c184/cryptography-45.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af3f92b1dc25621f5fad065288a44ac790c5798e986a34d393ab27d2b27fcff9", size = 3898150, upload-time = "2025-05-25T14:16:20.34Z" }, + { url = "https://files.pythonhosted.org/packages/8b/50/f256ab79c671fb066e47336706dc398c3b1e125f952e07d54ce82cf4011a/cryptography-45.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2f8f8f0b73b885ddd7f3d8c2b2234a7d3ba49002b0223f58cfde1bedd9563c56", size = 4466473, upload-time = "2025-05-25T14:16:22.605Z" }, + { url = "https://files.pythonhosted.org/packages/62/e7/312428336bb2df0848d0768ab5a062e11a32d18139447a76dfc19ada8eed/cryptography-45.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9cc80ce69032ffa528b5e16d217fa4d8d4bb7d6ba8659c1b4d74a1b0f4235fca", size = 4211890, upload-time = "2025-05-25T14:16:24.738Z" }, + { url = "https://files.pythonhosted.org/packages/e7/53/8a130e22c1e432b3c14896ec5eb7ac01fb53c6737e1d705df7e0efb647c6/cryptography-45.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c824c9281cb628015bfc3c59335163d4ca0540d49de4582d6c2637312907e4b1", size = 4466300, upload-time = "2025-05-25T14:16:26.768Z" }, + { url = "https://files.pythonhosted.org/packages/ba/75/6bb6579688ef805fd16a053005fce93944cdade465fc92ef32bbc5c40681/cryptography-45.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5833bb4355cb377ebd880457663a972cd044e7f49585aee39245c0d592904578", size = 4332483, upload-time = "2025-05-25T14:16:28.316Z" }, + { url = "https://files.pythonhosted.org/packages/2f/11/2538f4e1ce05c6c4f81f43c1ef2bd6de7ae5e24ee284460ff6c77e42ca77/cryptography-45.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bb5bf55dcb69f7067d80354d0a348368da907345a2c448b0babc4215ccd3497", size = 4573714, upload-time = "2025-05-25T14:16:30.474Z" }, + { url = "https://files.pythonhosted.org/packages/46/c7/c7d05d0e133a09fc677b8a87953815c522697bdf025e5cac13ba419e7240/cryptography-45.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5edcb90da1843df85292ef3a313513766a78fbbb83f584a5a58fb001a5a9d57", size = 4196181, upload-time = "2025-05-25T14:16:37.934Z" }, + { url = "https://files.pythonhosted.org/packages/08/7a/6ad3aa796b18a683657cef930a986fac0045417e2dc428fd336cfc45ba52/cryptography-45.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38deed72285c7ed699864f964a3f4cf11ab3fb38e8d39cfcd96710cd2b5bb716", size = 4423370, upload-time = "2025-05-25T14:16:39.502Z" }, + { url = "https://files.pythonhosted.org/packages/4f/58/ec1461bfcb393525f597ac6a10a63938d18775b7803324072974b41a926b/cryptography-45.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5555365a50efe1f486eed6ac7062c33b97ccef409f5970a0b6f205a7cfab59c8", size = 4197839, upload-time = "2025-05-25T14:16:41.322Z" }, + { url = "https://files.pythonhosted.org/packages/d4/3d/5185b117c32ad4f40846f579369a80e710d6146c2baa8ce09d01612750db/cryptography-45.0.3-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e4253ed8f5948a3589b3caee7ad9a5bf218ffd16869c516535325fece163dcc", size = 3886324, upload-time = "2025-05-25T14:16:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/67/85/caba91a57d291a2ad46e74016d1f83ac294f08128b26e2a81e9b4f2d2555/cryptography-45.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cfd84777b4b6684955ce86156cfb5e08d75e80dc2585e10d69e47f014f0a5342", size = 4450447, upload-time = "2025-05-25T14:16:44.759Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d1/164e3c9d559133a38279215c712b8ba38e77735d3412f37711b9f8f6f7e0/cryptography-45.0.3-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:a2b56de3417fd5f48773ad8e91abaa700b678dc7fe1e0c757e1ae340779acf7b", size = 4200576, upload-time = "2025-05-25T14:16:46.438Z" }, + { url = "https://files.pythonhosted.org/packages/71/7a/e002d5ce624ed46dfc32abe1deff32190f3ac47ede911789ee936f5a4255/cryptography-45.0.3-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:57a6500d459e8035e813bd8b51b671977fb149a8c95ed814989da682314d0782", size = 4450308, upload-time = "2025-05-25T14:16:48.228Z" }, + { url = "https://files.pythonhosted.org/packages/87/ad/3fbff9c28cf09b0a71e98af57d74f3662dea4a174b12acc493de00ea3f28/cryptography-45.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f22af3c78abfbc7cbcdf2c55d23c3e022e1a462ee2481011d518c7fb9c9f3d65", size = 4325125, upload-time = "2025-05-25T14:16:49.844Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b4/51417d0cc01802304c1984d76e9592f15e4801abd44ef7ba657060520bf0/cryptography-45.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:232954730c362638544758a8160c4ee1b832dc011d2c41a306ad8f7cccc5bb0b", size = 4560038, upload-time = "2025-05-25T14:16:51.398Z" }, +] + +[[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, upload-time = "2024-10-09T18:35:47.551Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973, upload-time = "2024-10-09T18:35:44.272Z" }, +] + +[[package]] +name = "docstamp" +source = { editable = "." } +dependencies = [ + { name = "click" }, + { name = "jinja2" }, + { name = "orjson" }, + { name = "pillow" }, + { name = "pypdf2" }, + { name = "qrcode" }, + { name = "svgutils" }, +] + +[package.dev-dependencies] +dev = [ + { name = "bumpversion" }, + { name = "hatch" }, + { name = "mypy" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "click", specifier = ">=7.0" }, + { name = "jinja2", specifier = ">=2.10" }, + { name = "orjson", specifier = ">=3.10.18" }, + { name = "pillow", specifier = ">=6.1.0" }, + { name = "pypdf2", specifier = ">=1.26.0" }, + { name = "qrcode", specifier = ">=6.1" }, + { name = "svgutils", specifier = "==0.3.1" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "bumpversion" }, + { name = "hatch" }, + { name = "mypy" }, + { name = "ruff" }, +] + +[[package]] +name = "filelock" +version = "3.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "hatch" +version = "1.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "hatchling" }, + { name = "httpx" }, + { name = "hyperlink" }, + { name = "keyring" }, + { name = "packaging" }, + { name = "pexpect" }, + { name = "platformdirs" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "tomli-w" }, + { name = "tomlkit" }, + { name = "userpath" }, + { name = "uv" }, + { name = "virtualenv" }, + { name = "zstandard" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/43/c0b37db0e857a44ce5ffdb7e8a9b8fa6425d0b74dea698fafcd9bddb50d1/hatch-1.14.1.tar.gz", hash = "sha256:ca1aff788f8596b0dd1f8f8dfe776443d2724a86b1976fabaf087406ba3d0713", size = 5188180, upload-time = "2025-04-07T04:16:04.522Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/40/19c0935bf9f25808541a0e3144ac459de696c5b6b6d4511a98d456c69604/hatch-1.14.1-py3-none-any.whl", hash = "sha256:39cdaa59e47ce0c5505d88a951f4324a9c5aafa17e4a877e2fde79b36ab66c21", size = 125770, upload-time = "2025-04-07T04:16:02.525Z" }, +] + +[[package]] +name = "hatchling" +version = "1.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "pathspec" }, + { name = "pluggy" }, + { name = "trove-classifiers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/8a/cc1debe3514da292094f1c3a700e4ca25442489731ef7c0814358816bb03/hatchling-1.27.0.tar.gz", hash = "sha256:971c296d9819abb3811112fc52c7a9751c8d381898f36533bb16f9791e941fd6", size = 54983, upload-time = "2024-12-15T17:08:11.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/e7/ae38d7a6dfba0533684e0b2136817d667588ae3ec984c1a4e5df5eb88482/hatchling-1.27.0-py3-none-any.whl", hash = "sha256:d3a2f3567c4f926ea39849cdf924c7e99e6686c9c8e288ae1037c8fa2a5d937b", size = 75794, upload-time = "2024-12-15T17:08:10.364Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[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, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "hyperlink" +version = "21.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/51/1947bd81d75af87e3bb9e34593a4cf118115a8feb451ce7a69044ef1412e/hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b", size = 140743, upload-time = "2021-01-08T05:51:20.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4", size = 74638, upload-time = "2021-01-08T05:51:22.906Z" }, +] + +[[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, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, +] + +[[package]] +name = "jaraco-context" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912, upload-time = "2024-08-20T03:39:27.358Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825, upload-time = "2024-08-20T03:39:25.966Z" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/23/9894b3df5d0a6eb44611c36aec777823fc2e07740dabbd0b810e19594013/jaraco_functools-4.1.0.tar.gz", hash = "sha256:70f7e0e2ae076498e212562325e805204fc092d7b4c17e0e86c959e249701a9d", size = 19159, upload-time = "2024-09-27T19:47:09.122Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/4f/24b319316142c44283d7540e76c7b5a6dbd5db623abd86bb7b3491c21018/jaraco.functools-4.1.0-py3-none-any.whl", hash = "sha256:ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649", size = 10187, upload-time = "2024-09-27T19:47:07.14Z" }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "keyring" +version = "25.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/09/d904a6e96f76ff214be59e7aa6ef7190008f52a0ab6689760a98de0bf37d/keyring-25.6.0.tar.gz", hash = "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66", size = 62750, upload-time = "2024-12-25T15:26:45.782Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/32/da7f44bcb1105d3e88a0b74ebdca50c59121d2ddf71c9e34ba47df7f3a56/keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd", size = 39085, upload-time = "2024-12-25T15:26:44.377Z" }, +] + +[[package]] +name = "lxml" +version = "5.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/3d/14e82fc7c8fb1b7761f7e748fd47e2ec8276d137b6acfe5a4bb73853e08f/lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd", size = 3679479, upload-time = "2025-04-23T01:50:29.322Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/4c/d101ace719ca6a4ec043eb516fcfcb1b396a9fccc4fcd9ef593df34ba0d5/lxml-5.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b5aff6f3e818e6bdbbb38e5967520f174b18f539c2b9de867b1e7fde6f8d95a4", size = 8127392, upload-time = "2025-04-23T01:46:04.09Z" }, + { url = "https://files.pythonhosted.org/packages/11/84/beddae0cec4dd9ddf46abf156f0af451c13019a0fa25d7445b655ba5ccb7/lxml-5.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942a5d73f739ad7c452bf739a62a0f83e2578afd6b8e5406308731f4ce78b16d", size = 4415103, upload-time = "2025-04-23T01:46:07.227Z" }, + { url = "https://files.pythonhosted.org/packages/d0/25/d0d93a4e763f0462cccd2b8a665bf1e4343dd788c76dcfefa289d46a38a9/lxml-5.4.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:460508a4b07364d6abf53acaa0a90b6d370fafde5693ef37602566613a9b0779", size = 5024224, upload-time = "2025-04-23T01:46:10.237Z" }, + { url = "https://files.pythonhosted.org/packages/31/ce/1df18fb8f7946e7f3388af378b1f34fcf253b94b9feedb2cec5969da8012/lxml-5.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529024ab3a505fed78fe3cc5ddc079464e709f6c892733e3f5842007cec8ac6e", size = 4769913, upload-time = "2025-04-23T01:46:12.757Z" }, + { url = "https://files.pythonhosted.org/packages/4e/62/f4a6c60ae7c40d43657f552f3045df05118636be1165b906d3423790447f/lxml-5.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ca56ebc2c474e8f3d5761debfd9283b8b18c76c4fc0967b74aeafba1f5647f9", size = 5290441, upload-time = "2025-04-23T01:46:16.037Z" }, + { url = "https://files.pythonhosted.org/packages/9e/aa/04f00009e1e3a77838c7fc948f161b5d2d5de1136b2b81c712a263829ea4/lxml-5.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a81e1196f0a5b4167a8dafe3a66aa67c4addac1b22dc47947abd5d5c7a3f24b5", size = 4820165, upload-time = "2025-04-23T01:46:19.137Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/e0b2f61fa2404bf0f1fdf1898377e5bd1b74cc9b2cf2c6ba8509b8f27990/lxml-5.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00b8686694423ddae324cf614e1b9659c2edb754de617703c3d29ff568448df5", size = 4932580, upload-time = "2025-04-23T01:46:21.963Z" }, + { url = "https://files.pythonhosted.org/packages/24/a2/8263f351b4ffe0ed3e32ea7b7830f845c795349034f912f490180d88a877/lxml-5.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c5681160758d3f6ac5b4fea370495c48aac0989d6a0f01bb9a72ad8ef5ab75c4", size = 4759493, upload-time = "2025-04-23T01:46:24.316Z" }, + { url = "https://files.pythonhosted.org/packages/05/00/41db052f279995c0e35c79d0f0fc9f8122d5b5e9630139c592a0b58c71b4/lxml-5.4.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:2dc191e60425ad70e75a68c9fd90ab284df64d9cd410ba8d2b641c0c45bc006e", size = 5324679, upload-time = "2025-04-23T01:46:27.097Z" }, + { url = "https://files.pythonhosted.org/packages/1d/be/ee99e6314cdef4587617d3b3b745f9356d9b7dd12a9663c5f3b5734b64ba/lxml-5.4.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:67f779374c6b9753ae0a0195a892a1c234ce8416e4448fe1e9f34746482070a7", size = 4890691, upload-time = "2025-04-23T01:46:30.009Z" }, + { url = "https://files.pythonhosted.org/packages/ad/36/239820114bf1d71f38f12208b9c58dec033cbcf80101cde006b9bde5cffd/lxml-5.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:79d5bfa9c1b455336f52343130b2067164040604e41f6dc4d8313867ed540079", size = 4955075, upload-time = "2025-04-23T01:46:32.33Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e1/1b795cc0b174efc9e13dbd078a9ff79a58728a033142bc6d70a1ee8fc34d/lxml-5.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3d3c30ba1c9b48c68489dc1829a6eede9873f52edca1dda900066542528d6b20", size = 4838680, upload-time = "2025-04-23T01:46:34.852Z" }, + { url = "https://files.pythonhosted.org/packages/72/48/3c198455ca108cec5ae3662ae8acd7fd99476812fd712bb17f1b39a0b589/lxml-5.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1af80c6316ae68aded77e91cd9d80648f7dd40406cef73df841aa3c36f6907c8", size = 5391253, upload-time = "2025-04-23T01:46:37.608Z" }, + { url = "https://files.pythonhosted.org/packages/d6/10/5bf51858971c51ec96cfc13e800a9951f3fd501686f4c18d7d84fe2d6352/lxml-5.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4d885698f5019abe0de3d352caf9466d5de2baded00a06ef3f1216c1a58ae78f", size = 5261651, upload-time = "2025-04-23T01:46:40.183Z" }, + { url = "https://files.pythonhosted.org/packages/2b/11/06710dd809205377da380546f91d2ac94bad9ff735a72b64ec029f706c85/lxml-5.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea53d51859b6c64e7c51d522c03cc2c48b9b5d6172126854cc7f01aa11f52bc", size = 5024315, upload-time = "2025-04-23T01:46:43.333Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b0/15b6217834b5e3a59ebf7f53125e08e318030e8cc0d7310355e6edac98ef/lxml-5.4.0-cp312-cp312-win32.whl", hash = "sha256:d90b729fd2732df28130c064aac9bb8aff14ba20baa4aee7bd0795ff1187545f", size = 3486149, upload-time = "2025-04-23T01:46:45.684Z" }, + { url = "https://files.pythonhosted.org/packages/91/1e/05ddcb57ad2f3069101611bd5f5084157d90861a2ef460bf42f45cced944/lxml-5.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1dc4ca99e89c335a7ed47d38964abcb36c5910790f9bd106f2a8fa2ee0b909d2", size = 3817095, upload-time = "2025-04-23T01:46:48.521Z" }, + { url = "https://files.pythonhosted.org/packages/87/cb/2ba1e9dd953415f58548506fa5549a7f373ae55e80c61c9041b7fd09a38a/lxml-5.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:773e27b62920199c6197130632c18fb7ead3257fce1ffb7d286912e56ddb79e0", size = 8110086, upload-time = "2025-04-23T01:46:52.218Z" }, + { url = "https://files.pythonhosted.org/packages/b5/3e/6602a4dca3ae344e8609914d6ab22e52ce42e3e1638c10967568c5c1450d/lxml-5.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ce9c671845de9699904b1e9df95acfe8dfc183f2310f163cdaa91a3535af95de", size = 4404613, upload-time = "2025-04-23T01:46:55.281Z" }, + { url = "https://files.pythonhosted.org/packages/4c/72/bf00988477d3bb452bef9436e45aeea82bb40cdfb4684b83c967c53909c7/lxml-5.4.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9454b8d8200ec99a224df8854786262b1bd6461f4280064c807303c642c05e76", size = 5012008, upload-time = "2025-04-23T01:46:57.817Z" }, + { url = "https://files.pythonhosted.org/packages/92/1f/93e42d93e9e7a44b2d3354c462cd784dbaaf350f7976b5d7c3f85d68d1b1/lxml-5.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cccd007d5c95279e529c146d095f1d39ac05139de26c098166c4beb9374b0f4d", size = 4760915, upload-time = "2025-04-23T01:47:00.745Z" }, + { url = "https://files.pythonhosted.org/packages/45/0b/363009390d0b461cf9976a499e83b68f792e4c32ecef092f3f9ef9c4ba54/lxml-5.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fce1294a0497edb034cb416ad3e77ecc89b313cff7adbee5334e4dc0d11f422", size = 5283890, upload-time = "2025-04-23T01:47:04.702Z" }, + { url = "https://files.pythonhosted.org/packages/19/dc/6056c332f9378ab476c88e301e6549a0454dbee8f0ae16847414f0eccb74/lxml-5.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24974f774f3a78ac12b95e3a20ef0931795ff04dbb16db81a90c37f589819551", size = 4812644, upload-time = "2025-04-23T01:47:07.833Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8a/f8c66bbb23ecb9048a46a5ef9b495fd23f7543df642dabeebcb2eeb66592/lxml-5.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:497cab4d8254c2a90bf988f162ace2ddbfdd806fce3bda3f581b9d24c852e03c", size = 4921817, upload-time = "2025-04-23T01:47:10.317Z" }, + { url = "https://files.pythonhosted.org/packages/04/57/2e537083c3f381f83d05d9b176f0d838a9e8961f7ed8ddce3f0217179ce3/lxml-5.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e794f698ae4c5084414efea0f5cc9f4ac562ec02d66e1484ff822ef97c2cadff", size = 4753916, upload-time = "2025-04-23T01:47:12.823Z" }, + { url = "https://files.pythonhosted.org/packages/d8/80/ea8c4072109a350848f1157ce83ccd9439601274035cd045ac31f47f3417/lxml-5.4.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:2c62891b1ea3094bb12097822b3d44b93fc6c325f2043c4d2736a8ff09e65f60", size = 5289274, upload-time = "2025-04-23T01:47:15.916Z" }, + { url = "https://files.pythonhosted.org/packages/b3/47/c4be287c48cdc304483457878a3f22999098b9a95f455e3c4bda7ec7fc72/lxml-5.4.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:142accb3e4d1edae4b392bd165a9abdee8a3c432a2cca193df995bc3886249c8", size = 4874757, upload-time = "2025-04-23T01:47:19.793Z" }, + { url = "https://files.pythonhosted.org/packages/2f/04/6ef935dc74e729932e39478e44d8cfe6a83550552eaa072b7c05f6f22488/lxml-5.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1a42b3a19346e5601d1b8296ff6ef3d76038058f311902edd574461e9c036982", size = 4947028, upload-time = "2025-04-23T01:47:22.401Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f9/c33fc8daa373ef8a7daddb53175289024512b6619bc9de36d77dca3df44b/lxml-5.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4291d3c409a17febf817259cb37bc62cb7eb398bcc95c1356947e2871911ae61", size = 4834487, upload-time = "2025-04-23T01:47:25.513Z" }, + { url = "https://files.pythonhosted.org/packages/8d/30/fc92bb595bcb878311e01b418b57d13900f84c2b94f6eca9e5073ea756e6/lxml-5.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4f5322cf38fe0e21c2d73901abf68e6329dc02a4994e483adbcf92b568a09a54", size = 5381688, upload-time = "2025-04-23T01:47:28.454Z" }, + { url = "https://files.pythonhosted.org/packages/43/d1/3ba7bd978ce28bba8e3da2c2e9d5ae3f8f521ad3f0ca6ea4788d086ba00d/lxml-5.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0be91891bdb06ebe65122aa6bf3fc94489960cf7e03033c6f83a90863b23c58b", size = 5242043, upload-time = "2025-04-23T01:47:31.208Z" }, + { url = "https://files.pythonhosted.org/packages/ee/cd/95fa2201041a610c4d08ddaf31d43b98ecc4b1d74b1e7245b1abdab443cb/lxml-5.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:15a665ad90054a3d4f397bc40f73948d48e36e4c09f9bcffc7d90c87410e478a", size = 5021569, upload-time = "2025-04-23T01:47:33.805Z" }, + { url = "https://files.pythonhosted.org/packages/2d/a6/31da006fead660b9512d08d23d31e93ad3477dd47cc42e3285f143443176/lxml-5.4.0-cp313-cp313-win32.whl", hash = "sha256:d5663bc1b471c79f5c833cffbc9b87d7bf13f87e055a5c86c363ccd2348d7e82", size = 3485270, upload-time = "2025-04-23T01:47:36.133Z" }, + { url = "https://files.pythonhosted.org/packages/fc/14/c115516c62a7d2499781d2d3d7215218c0731b2c940753bf9f9b7b73924d/lxml-5.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:bcb7a1096b4b6b24ce1ac24d4942ad98f983cd3810f9711bcd0293f43a9d8b9f", size = 3814606, upload-time = "2025-04-23T01:47:39.028Z" }, +] + +[[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, upload-time = "2023-06-03T06:41:14.443Z" } +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, upload-time = "2023-06-03T06:41:11.019Z" }, +] + +[[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, upload-time = "2024-10-18T15:21:54.129Z" } +wheels = [ + { 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, upload-time = "2024-10-18T15:21:13.777Z" }, + { 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, upload-time = "2024-10-18T15:21:14.822Z" }, + { 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, upload-time = "2024-10-18T15:21:15.642Z" }, + { 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, upload-time = "2024-10-18T15:21:17.133Z" }, + { 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, upload-time = "2024-10-18T15:21:18.064Z" }, + { 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, upload-time = "2024-10-18T15:21:18.859Z" }, + { 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, upload-time = "2024-10-18T15:21:19.671Z" }, + { 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, upload-time = "2024-10-18T15:21:20.971Z" }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, +] + +[[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, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "more-itertools" +version = "10.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/a0/834b0cebabbfc7e311f30b46c8188790a37f89fc8d756660346fe5abfd09/more_itertools-10.7.0.tar.gz", hash = "sha256:9fddd5403be01a94b204faadcff459ec3568cf110265d3c54323e1e866ad29d3", size = 127671, upload-time = "2025-04-22T14:17:41.838Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/9f/7ba6f94fc1e9ac3d2b853fdff3035fb2fa5afbed898c4a72b8a020610594/more_itertools-10.7.0-py3-none-any.whl", hash = "sha256:d43980384673cb07d2f7d2d918c616b30c659c089ee23953f601d6609c67510e", size = 65278, upload-time = "2025-04-22T14:17:40.49Z" }, +] + +[[package]] +name = "mypy" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d4/38/13c2f1abae94d5ea0354e146b95a1be9b2137a0d506728e0da037c4276f6/mypy-1.16.0.tar.gz", hash = "sha256:84b94283f817e2aa6350a14b4a8fb2a35a53c286f97c9d30f53b63620e7af8ab", size = 3323139, upload-time = "2025-05-29T13:46:12.532Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/cf/158e5055e60ca2be23aec54a3010f89dcffd788732634b344fc9cb1e85a0/mypy-1.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c5436d11e89a3ad16ce8afe752f0f373ae9620841c50883dc96f8b8805620b13", size = 11062927, upload-time = "2025-05-29T13:35:52.328Z" }, + { url = "https://files.pythonhosted.org/packages/94/34/cfff7a56be1609f5d10ef386342ce3494158e4d506516890142007e6472c/mypy-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f2622af30bf01d8fc36466231bdd203d120d7a599a6d88fb22bdcb9dbff84090", size = 10083082, upload-time = "2025-05-29T13:35:33.378Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7f/7242062ec6288c33d8ad89574df87c3903d394870e5e6ba1699317a65075/mypy-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d045d33c284e10a038f5e29faca055b90eee87da3fc63b8889085744ebabb5a1", size = 11828306, upload-time = "2025-05-29T13:21:02.164Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5f/b392f7b4f659f5b619ce5994c5c43caab3d80df2296ae54fa888b3d17f5a/mypy-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b4968f14f44c62e2ec4a038c8797a87315be8df7740dc3ee8d3bfe1c6bf5dba8", size = 12702764, upload-time = "2025-05-29T13:20:42.826Z" }, + { url = "https://files.pythonhosted.org/packages/9b/c0/7646ef3a00fa39ac9bc0938626d9ff29d19d733011be929cfea59d82d136/mypy-1.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb14a4a871bb8efb1e4a50360d4e3c8d6c601e7a31028a2c79f9bb659b63d730", size = 12896233, upload-time = "2025-05-29T13:18:37.446Z" }, + { url = "https://files.pythonhosted.org/packages/6d/38/52f4b808b3fef7f0ef840ee8ff6ce5b5d77381e65425758d515cdd4f5bb5/mypy-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:bd4e1ebe126152a7bbaa4daedd781c90c8f9643c79b9748caa270ad542f12bec", size = 9565547, upload-time = "2025-05-29T13:20:02.836Z" }, + { url = "https://files.pythonhosted.org/packages/97/9c/ca03bdbefbaa03b264b9318a98950a9c683e06472226b55472f96ebbc53d/mypy-1.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a9e056237c89f1587a3be1a3a70a06a698d25e2479b9a2f57325ddaaffc3567b", size = 11059753, upload-time = "2025-05-29T13:18:18.167Z" }, + { url = "https://files.pythonhosted.org/packages/36/92/79a969b8302cfe316027c88f7dc6fee70129490a370b3f6eb11d777749d0/mypy-1.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b07e107affb9ee6ce1f342c07f51552d126c32cd62955f59a7db94a51ad12c0", size = 10073338, upload-time = "2025-05-29T13:19:48.079Z" }, + { url = "https://files.pythonhosted.org/packages/14/9b/a943f09319167da0552d5cd722104096a9c99270719b1afeea60d11610aa/mypy-1.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6fb60cbd85dc65d4d63d37cb5c86f4e3a301ec605f606ae3a9173e5cf34997b", size = 11827764, upload-time = "2025-05-29T13:46:04.47Z" }, + { url = "https://files.pythonhosted.org/packages/ec/64/ff75e71c65a0cb6ee737287c7913ea155845a556c64144c65b811afdb9c7/mypy-1.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7e32297a437cc915599e0578fa6bc68ae6a8dc059c9e009c628e1c47f91495d", size = 12701356, upload-time = "2025-05-29T13:35:13.553Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ad/0e93c18987a1182c350f7a5fab70550852f9fabe30ecb63bfbe51b602074/mypy-1.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:afe420c9380ccec31e744e8baff0d406c846683681025db3531b32db56962d52", size = 12900745, upload-time = "2025-05-29T13:17:24.409Z" }, + { url = "https://files.pythonhosted.org/packages/28/5d/036c278d7a013e97e33f08c047fe5583ab4f1fc47c9a49f985f1cdd2a2d7/mypy-1.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:55f9076c6ce55dd3f8cd0c6fff26a008ca8e5131b89d5ba6d86bd3f47e736eeb", size = 9572200, upload-time = "2025-05-29T13:33:44.92Z" }, + { url = "https://files.pythonhosted.org/packages/99/a3/6ed10530dec8e0fdc890d81361260c9ef1f5e5c217ad8c9b21ecb2b8366b/mypy-1.16.0-py3-none-any.whl", hash = "sha256:29e1499864a3888bca5c1542f2d7232c6e586295183320caa95758fc84034031", size = 2265773, upload-time = "2025-05-29T13:35:18.762Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "orjson" +version = "3.10.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/0b/fea456a3ffe74e70ba30e01ec183a9b26bec4d497f61dcfce1b601059c60/orjson-3.10.18.tar.gz", hash = "sha256:e8da3947d92123eda795b68228cafe2724815621fe35e8e320a9e9593a4bcd53", size = 5422810, upload-time = "2025-04-29T23:30:08.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/1a/67236da0916c1a192d5f4ccbe10ec495367a726996ceb7614eaa687112f2/orjson-3.10.18-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:50c15557afb7f6d63bc6d6348e0337a880a04eaa9cd7c9d569bcb4e760a24753", size = 249184, upload-time = "2025-04-29T23:28:53.612Z" }, + { url = "https://files.pythonhosted.org/packages/b3/bc/c7f1db3b1d094dc0c6c83ed16b161a16c214aaa77f311118a93f647b32dc/orjson-3.10.18-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:356b076f1662c9813d5fa56db7d63ccceef4c271b1fb3dd522aca291375fcf17", size = 133279, upload-time = "2025-04-29T23:28:55.055Z" }, + { url = "https://files.pythonhosted.org/packages/af/84/664657cd14cc11f0d81e80e64766c7ba5c9b7fc1ec304117878cc1b4659c/orjson-3.10.18-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:559eb40a70a7494cd5beab2d73657262a74a2c59aff2068fdba8f0424ec5b39d", size = 136799, upload-time = "2025-04-29T23:28:56.828Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bb/f50039c5bb05a7ab024ed43ba25d0319e8722a0ac3babb0807e543349978/orjson-3.10.18-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f3c29eb9a81e2fbc6fd7ddcfba3e101ba92eaff455b8d602bf7511088bbc0eae", size = 132791, upload-time = "2025-04-29T23:28:58.751Z" }, + { url = "https://files.pythonhosted.org/packages/93/8c/ee74709fc072c3ee219784173ddfe46f699598a1723d9d49cbc78d66df65/orjson-3.10.18-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6612787e5b0756a171c7d81ba245ef63a3533a637c335aa7fcb8e665f4a0966f", size = 137059, upload-time = "2025-04-29T23:29:00.129Z" }, + { url = "https://files.pythonhosted.org/packages/6a/37/e6d3109ee004296c80426b5a62b47bcadd96a3deab7443e56507823588c5/orjson-3.10.18-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ac6bd7be0dcab5b702c9d43d25e70eb456dfd2e119d512447468f6405b4a69c", size = 138359, upload-time = "2025-04-29T23:29:01.704Z" }, + { url = "https://files.pythonhosted.org/packages/4f/5d/387dafae0e4691857c62bd02839a3bf3fa648eebd26185adfac58d09f207/orjson-3.10.18-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9f72f100cee8dde70100406d5c1abba515a7df926d4ed81e20a9730c062fe9ad", size = 142853, upload-time = "2025-04-29T23:29:03.576Z" }, + { url = "https://files.pythonhosted.org/packages/27/6f/875e8e282105350b9a5341c0222a13419758545ae32ad6e0fcf5f64d76aa/orjson-3.10.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dca85398d6d093dd41dc0983cbf54ab8e6afd1c547b6b8a311643917fbf4e0c", size = 133131, upload-time = "2025-04-29T23:29:05.753Z" }, + { url = "https://files.pythonhosted.org/packages/48/b2/73a1f0b4790dcb1e5a45f058f4f5dcadc8a85d90137b50d6bbc6afd0ae50/orjson-3.10.18-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:22748de2a07fcc8781a70edb887abf801bb6142e6236123ff93d12d92db3d406", size = 134834, upload-time = "2025-04-29T23:29:07.35Z" }, + { url = "https://files.pythonhosted.org/packages/56/f5/7ed133a5525add9c14dbdf17d011dd82206ca6840811d32ac52a35935d19/orjson-3.10.18-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3a83c9954a4107b9acd10291b7f12a6b29e35e8d43a414799906ea10e75438e6", size = 413368, upload-time = "2025-04-29T23:29:09.301Z" }, + { url = "https://files.pythonhosted.org/packages/11/7c/439654221ed9c3324bbac7bdf94cf06a971206b7b62327f11a52544e4982/orjson-3.10.18-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:303565c67a6c7b1f194c94632a4a39918e067bd6176a48bec697393865ce4f06", size = 153359, upload-time = "2025-04-29T23:29:10.813Z" }, + { url = "https://files.pythonhosted.org/packages/48/e7/d58074fa0cc9dd29a8fa2a6c8d5deebdfd82c6cfef72b0e4277c4017563a/orjson-3.10.18-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:86314fdb5053a2f5a5d881f03fca0219bfdf832912aa88d18676a5175c6916b5", size = 137466, upload-time = "2025-04-29T23:29:12.26Z" }, + { url = "https://files.pythonhosted.org/packages/57/4d/fe17581cf81fb70dfcef44e966aa4003360e4194d15a3f38cbffe873333a/orjson-3.10.18-cp312-cp312-win32.whl", hash = "sha256:187ec33bbec58c76dbd4066340067d9ece6e10067bb0cc074a21ae3300caa84e", size = 142683, upload-time = "2025-04-29T23:29:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/e6/22/469f62d25ab5f0f3aee256ea732e72dc3aab6d73bac777bd6277955bceef/orjson-3.10.18-cp312-cp312-win_amd64.whl", hash = "sha256:f9f94cf6d3f9cd720d641f8399e390e7411487e493962213390d1ae45c7814fc", size = 134754, upload-time = "2025-04-29T23:29:15.338Z" }, + { url = "https://files.pythonhosted.org/packages/10/b0/1040c447fac5b91bc1e9c004b69ee50abb0c1ffd0d24406e1350c58a7fcb/orjson-3.10.18-cp312-cp312-win_arm64.whl", hash = "sha256:3d600be83fe4514944500fa8c2a0a77099025ec6482e8087d7659e891f23058a", size = 131218, upload-time = "2025-04-29T23:29:17.324Z" }, + { url = "https://files.pythonhosted.org/packages/04/f0/8aedb6574b68096f3be8f74c0b56d36fd94bcf47e6c7ed47a7bd1474aaa8/orjson-3.10.18-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:69c34b9441b863175cc6a01f2935de994025e773f814412030f269da4f7be147", size = 249087, upload-time = "2025-04-29T23:29:19.083Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f7/7118f965541aeac6844fcb18d6988e111ac0d349c9b80cda53583e758908/orjson-3.10.18-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:1ebeda919725f9dbdb269f59bc94f861afbe2a27dce5608cdba2d92772364d1c", size = 133273, upload-time = "2025-04-29T23:29:20.602Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d9/839637cc06eaf528dd8127b36004247bf56e064501f68df9ee6fd56a88ee/orjson-3.10.18-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5adf5f4eed520a4959d29ea80192fa626ab9a20b2ea13f8f6dc58644f6927103", size = 136779, upload-time = "2025-04-29T23:29:22.062Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6d/f226ecfef31a1f0e7d6bf9a31a0bbaf384c7cbe3fce49cc9c2acc51f902a/orjson-3.10.18-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7592bb48a214e18cd670974f289520f12b7aed1fa0b2e2616b8ed9e069e08595", size = 132811, upload-time = "2025-04-29T23:29:23.602Z" }, + { url = "https://files.pythonhosted.org/packages/73/2d/371513d04143c85b681cf8f3bce743656eb5b640cb1f461dad750ac4b4d4/orjson-3.10.18-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f872bef9f042734110642b7a11937440797ace8c87527de25e0c53558b579ccc", size = 137018, upload-time = "2025-04-29T23:29:25.094Z" }, + { url = "https://files.pythonhosted.org/packages/69/cb/a4d37a30507b7a59bdc484e4a3253c8141bf756d4e13fcc1da760a0b00cb/orjson-3.10.18-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0315317601149c244cb3ecef246ef5861a64824ccbcb8018d32c66a60a84ffbc", size = 138368, upload-time = "2025-04-29T23:29:26.609Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ae/cd10883c48d912d216d541eb3db8b2433415fde67f620afe6f311f5cd2ca/orjson-3.10.18-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0da26957e77e9e55a6c2ce2e7182a36a6f6b180ab7189315cb0995ec362e049", size = 142840, upload-time = "2025-04-29T23:29:28.153Z" }, + { url = "https://files.pythonhosted.org/packages/6d/4c/2bda09855c6b5f2c055034c9eda1529967b042ff8d81a05005115c4e6772/orjson-3.10.18-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb70d489bc79b7519e5803e2cc4c72343c9dc1154258adf2f8925d0b60da7c58", size = 133135, upload-time = "2025-04-29T23:29:29.726Z" }, + { url = "https://files.pythonhosted.org/packages/13/4a/35971fd809a8896731930a80dfff0b8ff48eeb5d8b57bb4d0d525160017f/orjson-3.10.18-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e9e86a6af31b92299b00736c89caf63816f70a4001e750bda179e15564d7a034", size = 134810, upload-time = "2025-04-29T23:29:31.269Z" }, + { url = "https://files.pythonhosted.org/packages/99/70/0fa9e6310cda98365629182486ff37a1c6578e34c33992df271a476ea1cd/orjson-3.10.18-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:c382a5c0b5931a5fc5405053d36c1ce3fd561694738626c77ae0b1dfc0242ca1", size = 413491, upload-time = "2025-04-29T23:29:33.315Z" }, + { url = "https://files.pythonhosted.org/packages/32/cb/990a0e88498babddb74fb97855ae4fbd22a82960e9b06eab5775cac435da/orjson-3.10.18-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8e4b2ae732431127171b875cb2668f883e1234711d3c147ffd69fe5be51a8012", size = 153277, upload-time = "2025-04-29T23:29:34.946Z" }, + { url = "https://files.pythonhosted.org/packages/92/44/473248c3305bf782a384ed50dd8bc2d3cde1543d107138fd99b707480ca1/orjson-3.10.18-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d808e34ddb24fc29a4d4041dcfafbae13e129c93509b847b14432717d94b44f", size = 137367, upload-time = "2025-04-29T23:29:36.52Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fd/7f1d3edd4ffcd944a6a40e9f88af2197b619c931ac4d3cfba4798d4d3815/orjson-3.10.18-cp313-cp313-win32.whl", hash = "sha256:ad8eacbb5d904d5591f27dee4031e2c1db43d559edb8f91778efd642d70e6bea", size = 142687, upload-time = "2025-04-29T23:29:38.292Z" }, + { url = "https://files.pythonhosted.org/packages/4b/03/c75c6ad46be41c16f4cfe0352a2d1450546f3c09ad2c9d341110cd87b025/orjson-3.10.18-cp313-cp313-win_amd64.whl", hash = "sha256:aed411bcb68bf62e85588f2a7e03a6082cc42e5a2796e06e72a962d7c6310b52", size = 134794, upload-time = "2025-04-29T23:29:40.349Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/f53038a5a72cc4fd0b56c1eafb4ef64aec9685460d5ac34de98ca78b6e29/orjson-3.10.18-cp313-cp313-win_arm64.whl", hash = "sha256:f54c1385a0e6aba2f15a40d703b858bedad36ded0491e55d35d905b2c34a4cc3", size = 131186, upload-time = "2025-04-29T23:29:41.922Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, +] + +[[package]] +name = "pillow" +version = "11.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/cb/bb5c01fcd2a69335b86c22142b2bccfc3464087efb7fd382eee5ffc7fdf7/pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6", size = 47026707, upload-time = "2025-04-12T17:50:03.289Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/40/052610b15a1b8961f52537cc8326ca6a881408bc2bdad0d852edeb6ed33b/pillow-11.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78afba22027b4accef10dbd5eed84425930ba41b3ea0a86fa8d20baaf19d807f", size = 3190185, upload-time = "2025-04-12T17:48:00.417Z" }, + { url = "https://files.pythonhosted.org/packages/e5/7e/b86dbd35a5f938632093dc40d1682874c33dcfe832558fc80ca56bfcb774/pillow-11.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78092232a4ab376a35d68c4e6d5e00dfd73454bd12b230420025fbe178ee3b0b", size = 3030306, upload-time = "2025-04-12T17:48:02.391Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5c/467a161f9ed53e5eab51a42923c33051bf8d1a2af4626ac04f5166e58e0c/pillow-11.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a5f306095c6780c52e6bbb6109624b95c5b18e40aab1c3041da3e9e0cd3e2d", size = 4416121, upload-time = "2025-04-12T17:48:04.554Z" }, + { url = "https://files.pythonhosted.org/packages/62/73/972b7742e38ae0e2ac76ab137ca6005dcf877480da0d9d61d93b613065b4/pillow-11.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c7b29dbd4281923a2bfe562acb734cee96bbb129e96e6972d315ed9f232bef4", size = 4501707, upload-time = "2025-04-12T17:48:06.831Z" }, + { url = "https://files.pythonhosted.org/packages/e4/3a/427e4cb0b9e177efbc1a84798ed20498c4f233abde003c06d2650a6d60cb/pillow-11.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e645b020f3209a0181a418bffe7b4a93171eef6c4ef6cc20980b30bebf17b7d", size = 4522921, upload-time = "2025-04-12T17:48:09.229Z" }, + { url = "https://files.pythonhosted.org/packages/fe/7c/d8b1330458e4d2f3f45d9508796d7caf0c0d3764c00c823d10f6f1a3b76d/pillow-11.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2dbea1012ccb784a65349f57bbc93730b96e85b42e9bf7b01ef40443db720b4", size = 4612523, upload-time = "2025-04-12T17:48:11.631Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2f/65738384e0b1acf451de5a573d8153fe84103772d139e1e0bdf1596be2ea/pillow-11.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da3104c57bbd72948d75f6a9389e6727d2ab6333c3617f0a89d72d4940aa0443", size = 4587836, upload-time = "2025-04-12T17:48:13.592Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c5/e795c9f2ddf3debb2dedd0df889f2fe4b053308bb59a3cc02a0cd144d641/pillow-11.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:598174aef4589af795f66f9caab87ba4ff860ce08cd5bb447c6fc553ffee603c", size = 4669390, upload-time = "2025-04-12T17:48:15.938Z" }, + { url = "https://files.pythonhosted.org/packages/96/ae/ca0099a3995976a9fce2f423166f7bff9b12244afdc7520f6ed38911539a/pillow-11.2.1-cp312-cp312-win32.whl", hash = "sha256:1d535df14716e7f8776b9e7fee118576d65572b4aad3ed639be9e4fa88a1cad3", size = 2332309, upload-time = "2025-04-12T17:48:17.885Z" }, + { url = "https://files.pythonhosted.org/packages/7c/18/24bff2ad716257fc03da964c5e8f05d9790a779a8895d6566e493ccf0189/pillow-11.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:14e33b28bf17c7a38eede290f77db7c664e4eb01f7869e37fa98a5aa95978941", size = 2676768, upload-time = "2025-04-12T17:48:19.655Z" }, + { url = "https://files.pythonhosted.org/packages/da/bb/e8d656c9543276517ee40184aaa39dcb41e683bca121022f9323ae11b39d/pillow-11.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb", size = 2415087, upload-time = "2025-04-12T17:48:21.991Z" }, + { url = "https://files.pythonhosted.org/packages/36/9c/447528ee3776e7ab8897fe33697a7ff3f0475bb490c5ac1456a03dc57956/pillow-11.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28", size = 3190098, upload-time = "2025-04-12T17:48:23.915Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/29d5cd052f7566a63e5b506fac9c60526e9ecc553825551333e1e18a4858/pillow-11.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830", size = 3030166, upload-time = "2025-04-12T17:48:25.738Z" }, + { url = "https://files.pythonhosted.org/packages/71/5d/446ee132ad35e7600652133f9c2840b4799bbd8e4adba881284860da0a36/pillow-11.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0", size = 4408674, upload-time = "2025-04-12T17:48:27.908Z" }, + { url = "https://files.pythonhosted.org/packages/69/5f/cbe509c0ddf91cc3a03bbacf40e5c2339c4912d16458fcb797bb47bcb269/pillow-11.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1", size = 4496005, upload-time = "2025-04-12T17:48:29.888Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b3/dd4338d8fb8a5f312021f2977fb8198a1184893f9b00b02b75d565c33b51/pillow-11.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f", size = 4518707, upload-time = "2025-04-12T17:48:31.874Z" }, + { url = "https://files.pythonhosted.org/packages/13/eb/2552ecebc0b887f539111c2cd241f538b8ff5891b8903dfe672e997529be/pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155", size = 4610008, upload-time = "2025-04-12T17:48:34.422Z" }, + { url = "https://files.pythonhosted.org/packages/72/d1/924ce51bea494cb6e7959522d69d7b1c7e74f6821d84c63c3dc430cbbf3b/pillow-11.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14", size = 4585420, upload-time = "2025-04-12T17:48:37.641Z" }, + { url = "https://files.pythonhosted.org/packages/43/ab/8f81312d255d713b99ca37479a4cb4b0f48195e530cdc1611990eb8fd04b/pillow-11.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b", size = 4667655, upload-time = "2025-04-12T17:48:39.652Z" }, + { url = "https://files.pythonhosted.org/packages/94/86/8f2e9d2dc3d308dfd137a07fe1cc478df0a23d42a6c4093b087e738e4827/pillow-11.2.1-cp313-cp313-win32.whl", hash = "sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2", size = 2332329, upload-time = "2025-04-12T17:48:41.765Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ec/1179083b8d6067a613e4d595359b5fdea65d0a3b7ad623fee906e1b3c4d2/pillow-11.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691", size = 2676388, upload-time = "2025-04-12T17:48:43.625Z" }, + { url = "https://files.pythonhosted.org/packages/23/f1/2fc1e1e294de897df39fa8622d829b8828ddad938b0eaea256d65b84dd72/pillow-11.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c", size = 2414950, upload-time = "2025-04-12T17:48:45.475Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3e/c328c48b3f0ead7bab765a84b4977acb29f101d10e4ef57a5e3400447c03/pillow-11.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22", size = 3192759, upload-time = "2025-04-12T17:48:47.866Z" }, + { url = "https://files.pythonhosted.org/packages/18/0e/1c68532d833fc8b9f404d3a642991441d9058eccd5606eab31617f29b6d4/pillow-11.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7", size = 3033284, upload-time = "2025-04-12T17:48:50.189Z" }, + { url = "https://files.pythonhosted.org/packages/b7/cb/6faf3fb1e7705fd2db74e070f3bf6f88693601b0ed8e81049a8266de4754/pillow-11.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16", size = 4445826, upload-time = "2025-04-12T17:48:52.346Z" }, + { url = "https://files.pythonhosted.org/packages/07/94/8be03d50b70ca47fb434a358919d6a8d6580f282bbb7af7e4aa40103461d/pillow-11.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b", size = 4527329, upload-time = "2025-04-12T17:48:54.403Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a4/bfe78777076dc405e3bd2080bc32da5ab3945b5a25dc5d8acaa9de64a162/pillow-11.2.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406", size = 4549049, upload-time = "2025-04-12T17:48:56.383Z" }, + { url = "https://files.pythonhosted.org/packages/65/4d/eaf9068dc687c24979e977ce5677e253624bd8b616b286f543f0c1b91662/pillow-11.2.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91", size = 4635408, upload-time = "2025-04-12T17:48:58.782Z" }, + { url = "https://files.pythonhosted.org/packages/1d/26/0fd443365d9c63bc79feb219f97d935cd4b93af28353cba78d8e77b61719/pillow-11.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751", size = 4614863, upload-time = "2025-04-12T17:49:00.709Z" }, + { url = "https://files.pythonhosted.org/packages/49/65/dca4d2506be482c2c6641cacdba5c602bc76d8ceb618fd37de855653a419/pillow-11.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9", size = 4692938, upload-time = "2025-04-12T17:49:02.946Z" }, + { url = "https://files.pythonhosted.org/packages/b3/92/1ca0c3f09233bd7decf8f7105a1c4e3162fb9142128c74adad0fb361b7eb/pillow-11.2.1-cp313-cp313t-win32.whl", hash = "sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd", size = 2335774, upload-time = "2025-04-12T17:49:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ac/77525347cb43b83ae905ffe257bbe2cc6fd23acb9796639a1f56aa59d191/pillow-11.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e", size = 2681895, upload-time = "2025-04-12T17:49:06.635Z" }, + { url = "https://files.pythonhosted.org/packages/67/32/32dc030cfa91ca0fc52baebbba2e009bb001122a1daa8b6a79ad830b38d3/pillow-11.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681", size = 2417234, upload-time = "2025-04-12T17:49:08.399Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, +] + +[[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, upload-time = "2024-03-30T13:22:22.564Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, +] + +[[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, upload-time = "2025-01-06T17:26:30.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, +] + +[[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, upload-time = "2022-12-31T10:36:13.13Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/5e/c86a5643653825d3c913719e788e41386bee415c2b87b4f955432f2de6b2/pypdf2-3.0.1-py3-none-any.whl", hash = "sha256:d16e4205cfee272fbdc0568b68d82be796540b1537508cef59388f839c191928", size = 232572, upload-time = "2022-12-31T10:36:10.327Z" }, +] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + +[[package]] +name = "qrcode" +version = "8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/b2/7fc2931bfae0af02d5f53b174e9cf701adbb35f39d69c2af63d4a39f81a9/qrcode-8.2.tar.gz", hash = "sha256:35c3f2a4172b33136ab9f6b3ef1c00260dd2f66f858f24d88418a015f446506c", size = 43317, upload-time = "2025-05-01T15:44:24.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/b8/d2d6d731733f51684bbf76bf34dab3b70a9148e8f2cef2bb544fccec681a/qrcode-8.2-py3-none-any.whl", hash = "sha256:16e64e0716c14960108e85d853062c9e8bba5ca8252c0b4d0231b9df4060ff4f", size = 45986, upload-time = "2025-05-01T15:44:22.781Z" }, +] + +[[package]] +name = "rich" +version = "14.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" }, +] + +[[package]] +name = "ruff" +version = "0.11.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/0a/92416b159ec00cdf11e5882a9d80d29bf84bba3dbebc51c4898bfbca1da6/ruff-0.11.12.tar.gz", hash = "sha256:43cf7f69c7d7c7d7513b9d59c5d8cafd704e05944f978614aa9faff6ac202603", size = 4202289, upload-time = "2025-05-29T13:31:40.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/cc/53eb79f012d15e136d40a8e8fc519ba8f55a057f60b29c2df34efd47c6e3/ruff-0.11.12-py3-none-linux_armv6l.whl", hash = "sha256:c7680aa2f0d4c4f43353d1e72123955c7a2159b8646cd43402de6d4a3a25d7cc", size = 10285597, upload-time = "2025-05-29T13:30:57.539Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d7/73386e9fb0232b015a23f62fea7503f96e29c29e6c45461d4a73bac74df9/ruff-0.11.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2cad64843da9f134565c20bcc430642de897b8ea02e2e79e6e02a76b8dcad7c3", size = 11053154, upload-time = "2025-05-29T13:31:00.865Z" }, + { url = "https://files.pythonhosted.org/packages/4e/eb/3eae144c5114e92deb65a0cb2c72326c8469e14991e9bc3ec0349da1331c/ruff-0.11.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9b6886b524a1c659cee1758140138455d3c029783d1b9e643f3624a5ee0cb0aa", size = 10403048, upload-time = "2025-05-29T13:31:03.413Z" }, + { url = "https://files.pythonhosted.org/packages/29/64/20c54b20e58b1058db6689e94731f2a22e9f7abab74e1a758dfba058b6ca/ruff-0.11.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc3a3690aad6e86c1958d3ec3c38c4594b6ecec75c1f531e84160bd827b2012", size = 10597062, upload-time = "2025-05-29T13:31:05.539Z" }, + { url = "https://files.pythonhosted.org/packages/29/3a/79fa6a9a39422a400564ca7233a689a151f1039110f0bbbabcb38106883a/ruff-0.11.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f97fdbc2549f456c65b3b0048560d44ddd540db1f27c778a938371424b49fe4a", size = 10155152, upload-time = "2025-05-29T13:31:07.986Z" }, + { url = "https://files.pythonhosted.org/packages/e5/a4/22c2c97b2340aa968af3a39bc38045e78d36abd4ed3fa2bde91c31e712e3/ruff-0.11.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74adf84960236961090e2d1348c1a67d940fd12e811a33fb3d107df61eef8fc7", size = 11723067, upload-time = "2025-05-29T13:31:10.57Z" }, + { url = "https://files.pythonhosted.org/packages/bc/cf/3e452fbd9597bcd8058856ecd42b22751749d07935793a1856d988154151/ruff-0.11.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b56697e5b8bcf1d61293ccfe63873aba08fdbcbbba839fc046ec5926bdb25a3a", size = 12460807, upload-time = "2025-05-29T13:31:12.88Z" }, + { url = "https://files.pythonhosted.org/packages/2f/ec/8f170381a15e1eb7d93cb4feef8d17334d5a1eb33fee273aee5d1f8241a3/ruff-0.11.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d47afa45e7b0eaf5e5969c6b39cbd108be83910b5c74626247e366fd7a36a13", size = 12063261, upload-time = "2025-05-29T13:31:15.236Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/57208f8c0a8153a14652a85f4116c0002148e83770d7a41f2e90b52d2b4e/ruff-0.11.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bf9603fe1bf949de8b09a2da896f05c01ed7a187f4a386cdba6760e7f61be", size = 11329601, upload-time = "2025-05-29T13:31:18.68Z" }, + { url = "https://files.pythonhosted.org/packages/c3/56/edf942f7fdac5888094d9ffa303f12096f1a93eb46570bcf5f14c0c70880/ruff-0.11.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08033320e979df3b20dba567c62f69c45e01df708b0f9c83912d7abd3e0801cd", size = 11522186, upload-time = "2025-05-29T13:31:21.216Z" }, + { url = "https://files.pythonhosted.org/packages/ed/63/79ffef65246911ed7e2290aeece48739d9603b3a35f9529fec0fc6c26400/ruff-0.11.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:929b7706584f5bfd61d67d5070f399057d07c70585fa8c4491d78ada452d3bef", size = 10449032, upload-time = "2025-05-29T13:31:23.417Z" }, + { url = "https://files.pythonhosted.org/packages/88/19/8c9d4d8a1c2a3f5a1ea45a64b42593d50e28b8e038f1aafd65d6b43647f3/ruff-0.11.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7de4a73205dc5756b8e09ee3ed67c38312dce1aa28972b93150f5751199981b5", size = 10129370, upload-time = "2025-05-29T13:31:25.777Z" }, + { url = "https://files.pythonhosted.org/packages/bc/0f/2d15533eaa18f460530a857e1778900cd867ded67f16c85723569d54e410/ruff-0.11.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2635c2a90ac1b8ca9e93b70af59dfd1dd2026a40e2d6eebaa3efb0465dd9cf02", size = 11123529, upload-time = "2025-05-29T13:31:28.396Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e2/4c2ac669534bdded835356813f48ea33cfb3a947dc47f270038364587088/ruff-0.11.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d05d6a78a89166f03f03a198ecc9d18779076ad0eec476819467acb401028c0c", size = 11577642, upload-time = "2025-05-29T13:31:30.647Z" }, + { url = "https://files.pythonhosted.org/packages/a7/9b/c9ddf7f924d5617a1c94a93ba595f4b24cb5bc50e98b94433ab3f7ad27e5/ruff-0.11.12-py3-none-win32.whl", hash = "sha256:f5a07f49767c4be4772d161bfc049c1f242db0cfe1bd976e0f0886732a4765d6", size = 10475511, upload-time = "2025-05-29T13:31:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d6/74fb6d3470c1aada019ffff33c0f9210af746cca0a4de19a1f10ce54968a/ruff-0.11.12-py3-none-win_amd64.whl", hash = "sha256:5a4d9f8030d8c3a45df201d7fb3ed38d0219bccd7955268e863ee4a115fa0832", size = 11523573, upload-time = "2025-05-29T13:31:35.782Z" }, + { url = "https://files.pythonhosted.org/packages/44/42/d58086ec20f52d2b0140752ae54b355ea2be2ed46f914231136dd1effcc7/ruff-0.11.12-py3-none-win_arm64.whl", hash = "sha256:65194e37853158d368e333ba282217941029a28ea90913c67e558c611d04daa5", size = 10697770, upload-time = "2025-05-29T13:31:38.009Z" }, +] + +[[package]] +name = "secretstorage" +version = "3.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/a4/f48c9d79cb507ed1373477dbceaba7401fd8a23af63b837fa61f1dcd3691/SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", size = 19739, upload-time = "2022-08-13T16:22:46.976Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/24/b4293291fa1dd830f353d2cb163295742fa87f179fcc8a20a306a81978b7/SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99", size = 15221, upload-time = "2022-08-13T16:22:44.457Z" }, +] + +[[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, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[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, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "svgutils" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lxml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/35/21e59c17e0d435b953b0c1a8ffd914f4bf3411b52ae04030c0c4153ef929/svgutils-0.3.1.tar.gz", hash = "sha256:cd52474765fd460ad2389947f77589de96142f6f0ce3f61e08ccfabeac2ff8af", size = 8959, upload-time = "2018-10-24T09:28:22.27Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/da/4f7a31a55c247e304a338716e75d761f3dc9b50b220fcfaad7398668367e/svgutils-0.3.1-py2.py3-none-any.whl", hash = "sha256:6c136225fd210b844a2a90011563195fba4968d2d5cc96e737784a4728850f3a", size = 10402, upload-time = "2018-10-24T09:28:15.527Z" }, +] + +[[package]] +name = "tomli-w" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, +] + +[[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, upload-time = "2024-08-14T08:19:41.488Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/b6/a447b5e4ec71e13871be01ba81f5dfc9d0af7e473da256ff46bc0e24026f/tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde", size = 37955, upload-time = "2024-08-14T08:19:40.05Z" }, +] + +[[package]] +name = "trove-classifiers" +version = "2025.5.9.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/04/1cd43f72c241fedcf0d9a18d0783953ee301eac9e5d9db1df0f0f089d9af/trove_classifiers-2025.5.9.12.tar.gz", hash = "sha256:7ca7c8a7a76e2cd314468c677c69d12cc2357711fcab4a60f87994c1589e5cb5", size = 16940, upload-time = "2025-05-09T12:04:48.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/ef/c6deb083748be3bcad6f471b6ae983950c161890bf5ae1b2af80cc56c530/trove_classifiers-2025.5.9.12-py3-none-any.whl", hash = "sha256:e381c05537adac78881c8fa345fd0e9970159f4e4a04fcc42cfd3129cca640ce", size = 14119, upload-time = "2025-05-09T12:04:46.38Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, +] + +[[package]] +name = "userpath" +version = "1.9.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d5/b7/30753098208505d7ff9be5b3a32112fb8a4cb3ddfccbbb7ba9973f2e29ff/userpath-1.9.2.tar.gz", hash = "sha256:6c52288dab069257cc831846d15d48133522455d4677ee69a9781f11dbefd815", size = 11140, upload-time = "2024-02-29T21:39:08.742Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl", hash = "sha256:2cbf01a23d655a1ff8fc166dfb78da1b641d1ceabf0fe5f970767d380b14e89d", size = 9065, upload-time = "2024-02-29T21:39:07.551Z" }, +] + +[[package]] +name = "uv" +version = "0.7.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/7c/8621d5928111f985196dc75c50a64147b3bad39f36164686f24d45581367/uv-0.7.9.tar.gz", hash = "sha256:baac54e49f3b0d05ee83f534fdcb27b91d2923c585bf349a1532ca25d62c216f", size = 3272882, upload-time = "2025-05-30T19:54:33.003Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/7a/e4d12029e16f30279ef48f387545f8f3974dc3c4c9d8ef59c381ae7e6a7d/uv-0.7.9-py3-none-linux_armv6l.whl", hash = "sha256:0f8c53d411f95cec2fa19471c23b41ec456fc0d5f2efca96480d94e0c34026c2", size = 16746809, upload-time = "2025-05-30T19:53:35.447Z" }, + { url = "https://files.pythonhosted.org/packages/fc/85/8df3ca683e1a260117efa31373e91e1c03a4862b7add865662f60a967fdf/uv-0.7.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:85c1a63669e49b825923fc876b7467cc3c20d4aa010f522c0ac8b0f30ce2b18e", size = 16821006, upload-time = "2025-05-30T19:53:40.102Z" }, + { url = "https://files.pythonhosted.org/packages/77/d4/c40502ec8f5575798b7ec13ac38c0d5ded84cc32129c1d74a47f8cb7bc0a/uv-0.7.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:aa10c61668f94515acf93f31dbb8de41b1f2e7a9c41db828f2448cef786498ff", size = 15600148, upload-time = "2025-05-30T19:53:43.513Z" }, + { url = "https://files.pythonhosted.org/packages/4f/dd/4deec6d5b556f4033d6bcc35d6aad70c08acea3f5da749cb34112dced5da/uv-0.7.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:9de67ca9ea97db71e5697c1320508e25679fb68d4ee2cea27bbeac499a6bad56", size = 16038119, upload-time = "2025-05-30T19:53:46.504Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c5/2c23763e18566a9a7767738714791203cc97a7530979f61e0fd32d8473a2/uv-0.7.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13ce63524f88228152edf8a9c1c07cecc07d69a2853b32ecc02ac73538aaa5c1", size = 16467257, upload-time = "2025-05-30T19:53:49.592Z" }, + { url = "https://files.pythonhosted.org/packages/da/94/f452d0093f466f9f81a2ede3ea2d48632237b79eb1dc595c7c91be309de5/uv-0.7.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3453b7bb65eaea87c9129e27bff701007a8bd1a563249982a1ede7ec4357ced6", size = 17170719, upload-time = "2025-05-30T19:53:52.828Z" }, + { url = "https://files.pythonhosted.org/packages/69/bf/e15ef77520e9bbf00d29a3b639dfaf4fe63996863d6db00c53eba19535c7/uv-0.7.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d7b1e36a8b39600d0f0333bf50c966e83beeaaee1a38380ccb0f16ab45f351c3", size = 18052903, upload-time = "2025-05-30T19:53:56.237Z" }, + { url = "https://files.pythonhosted.org/packages/32/9f/ebf3f9910121ef037c0fe9e7e7fb5f1c25b77d41a65a029d5cbcd85cc886/uv-0.7.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ab412ed3d415f07192805788669c8a89755086cdd6fe9f021e1ba21781728031", size = 17771828, upload-time = "2025-05-30T19:53:59.561Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6c/82b4cd471432e721c239ddde2ebee2e674238f3bd88e279e6c71f3cbc775/uv-0.7.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbeb229ee86f69913f5f9236ff1b8ccbae212f559d7f029f8432fa8d9abcc7e0", size = 17886161, upload-time = "2025-05-30T19:54:02.865Z" }, + { url = "https://files.pythonhosted.org/packages/63/e2/922d2eed25647b50a7257a7bfea10c36d9ff910d1451f9a1ba5e31766f41/uv-0.7.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1d654a14d632ecb078969ae7252d89dd98c89205df567a1eff18b5f078a6d00", size = 17442630, upload-time = "2025-05-30T19:54:06.519Z" }, + { url = "https://files.pythonhosted.org/packages/96/b8/45a5598cc8d466bb1669ccf0fc4f556719babfdb7a1983edc24967cb3845/uv-0.7.9-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:f5f47e93a5f948f431ca55d765af6e818c925500807539b976bfda7f94369aa9", size = 16299207, upload-time = "2025-05-30T19:54:09.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/35/7e70639cd175f340138c88290c819214a496dfc52461f30f71e51e776293/uv-0.7.9-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:267fe25ad3adf024e13617be9fc99bedebf96bf726c6140e48d856e844f21af4", size = 16427594, upload-time = "2025-05-30T19:54:13.318Z" }, + { url = "https://files.pythonhosted.org/packages/5e/f6/90fe538370bc60509cca942b703bca06c06c160ec09816ea6946882278d1/uv-0.7.9-py3-none-musllinux_1_1_i686.whl", hash = "sha256:473d3c6ee07588cff8319079d9225fb393ed177d8d57186fce0d7c1aebff79c0", size = 16751451, upload-time = "2025-05-30T19:54:16.833Z" }, + { url = "https://files.pythonhosted.org/packages/09/cb/c099aba21fb22e50713b42e874075a5b60c6b4d141cc3868ae22f505baa7/uv-0.7.9-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:19792c88058894c10f0370a5e5492bb4a7e6302c439fed9882e73ba2e4b231ef", size = 17594496, upload-time = "2025-05-30T19:54:20.383Z" }, + { url = "https://files.pythonhosted.org/packages/c0/2e/e35b2c5669533075987e1d74da45af891890ae5faee031f90997ed81cada/uv-0.7.9-py3-none-win32.whl", hash = "sha256:298e9b3c65742edcb3097c2cf3f62ec847df174a7c62c85fe139dddaa1b9ab65", size = 17121149, upload-time = "2025-05-30T19:54:23.608Z" }, + { url = "https://files.pythonhosted.org/packages/33/8e/d10425711156d0d5d9a28299950acb3ab4a3987b3150a3c871ac95ce2fdd/uv-0.7.9-py3-none-win_amd64.whl", hash = "sha256:82d76ea988ff1347158c6de46a571b1db7d344219e452bd7b3339c21ec37cfd8", size = 18622895, upload-time = "2025-05-30T19:54:27.427Z" }, + { url = "https://files.pythonhosted.org/packages/72/77/cac29a8fb608b5613b7a0863ec6bd7c2517f3a80b94c419e9d890c12257e/uv-0.7.9-py3-none-win_arm64.whl", hash = "sha256:4d419bcc3138fd787ce77305f1a09e2a984766e0804c6e5a2b54adfa55d2439a", size = 17316542, upload-time = "2025-05-30T19:54:30.697Z" }, +] + +[[package]] +name = "virtualenv" +version = "20.31.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/2c/444f465fb2c65f40c3a104fd0c495184c4f2336d65baf398e3c75d72ea94/virtualenv-20.31.2.tar.gz", hash = "sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af", size = 6076316, upload-time = "2025-05-08T17:58:23.811Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/40/b1c265d4b2b62b58576588510fc4d1fe60a86319c8de99fd8e9fec617d2c/virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11", size = 6057982, upload-time = "2025-05-08T17:58:21.15Z" }, +] + +[[package]] +name = "zstandard" +version = "0.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation == 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/f6/2ac0287b442160a89d726b17a9184a4c615bb5237db763791a7fd16d9df1/zstandard-0.23.0.tar.gz", hash = "sha256:b2d8c62d08e7255f68f7a740bae85b3c9b8e5466baa9cbf7f57f1cde0ac6bc09", size = 681701, upload-time = "2024-07-15T00:18:06.141Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/83/f23338c963bd9de687d47bf32efe9fd30164e722ba27fb59df33e6b1719b/zstandard-0.23.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b4567955a6bc1b20e9c31612e615af6b53733491aeaa19a6b3b37f3b65477094", size = 788713, upload-time = "2024-07-15T00:15:35.815Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b3/1a028f6750fd9227ee0b937a278a434ab7f7fdc3066c3173f64366fe2466/zstandard-0.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e172f57cd78c20f13a3415cc8dfe24bf388614324d25539146594c16d78fcc8", size = 633459, upload-time = "2024-07-15T00:15:37.995Z" }, + { url = "https://files.pythonhosted.org/packages/26/af/36d89aae0c1f95a0a98e50711bc5d92c144939efc1f81a2fcd3e78d7f4c1/zstandard-0.23.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0e166f698c5a3e914947388c162be2583e0c638a4703fc6a543e23a88dea3c1", size = 4945707, upload-time = "2024-07-15T00:15:39.872Z" }, + { url = "https://files.pythonhosted.org/packages/cd/2e/2051f5c772f4dfc0aae3741d5fc72c3dcfe3aaeb461cc231668a4db1ce14/zstandard-0.23.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12a289832e520c6bd4dcaad68e944b86da3bad0d339ef7989fb7e88f92e96072", size = 5306545, upload-time = "2024-07-15T00:15:41.75Z" }, + { url = "https://files.pythonhosted.org/packages/0a/9e/a11c97b087f89cab030fa71206963090d2fecd8eb83e67bb8f3ffb84c024/zstandard-0.23.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d50d31bfedd53a928fed6707b15a8dbeef011bb6366297cc435accc888b27c20", size = 5337533, upload-time = "2024-07-15T00:15:44.114Z" }, + { url = "https://files.pythonhosted.org/packages/fc/79/edeb217c57fe1bf16d890aa91a1c2c96b28c07b46afed54a5dcf310c3f6f/zstandard-0.23.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72c68dda124a1a138340fb62fa21b9bf4848437d9ca60bd35db36f2d3345f373", size = 5436510, upload-time = "2024-07-15T00:15:46.509Z" }, + { url = "https://files.pythonhosted.org/packages/81/4f/c21383d97cb7a422ddf1ae824b53ce4b51063d0eeb2afa757eb40804a8ef/zstandard-0.23.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53dd9d5e3d29f95acd5de6802e909ada8d8d8cfa37a3ac64836f3bc4bc5512db", size = 4859973, upload-time = "2024-07-15T00:15:49.939Z" }, + { url = "https://files.pythonhosted.org/packages/ab/15/08d22e87753304405ccac8be2493a495f529edd81d39a0870621462276ef/zstandard-0.23.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6a41c120c3dbc0d81a8e8adc73312d668cd34acd7725f036992b1b72d22c1772", size = 4936968, upload-time = "2024-07-15T00:15:52.025Z" }, + { url = "https://files.pythonhosted.org/packages/eb/fa/f3670a597949fe7dcf38119a39f7da49a8a84a6f0b1a2e46b2f71a0ab83f/zstandard-0.23.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:40b33d93c6eddf02d2c19f5773196068d875c41ca25730e8288e9b672897c105", size = 5467179, upload-time = "2024-07-15T00:15:54.971Z" }, + { url = "https://files.pythonhosted.org/packages/4e/a9/dad2ab22020211e380adc477a1dbf9f109b1f8d94c614944843e20dc2a99/zstandard-0.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9206649ec587e6b02bd124fb7799b86cddec350f6f6c14bc82a2b70183e708ba", size = 4848577, upload-time = "2024-07-15T00:15:57.634Z" }, + { url = "https://files.pythonhosted.org/packages/08/03/dd28b4484b0770f1e23478413e01bee476ae8227bbc81561f9c329e12564/zstandard-0.23.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76e79bc28a65f467e0409098fa2c4376931fd3207fbeb6b956c7c476d53746dd", size = 4693899, upload-time = "2024-07-15T00:16:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/2b/64/3da7497eb635d025841e958bcd66a86117ae320c3b14b0ae86e9e8627518/zstandard-0.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:66b689c107857eceabf2cf3d3fc699c3c0fe8ccd18df2219d978c0283e4c508a", size = 5199964, upload-time = "2024-07-15T00:16:03.669Z" }, + { url = "https://files.pythonhosted.org/packages/43/a4/d82decbab158a0e8a6ebb7fc98bc4d903266bce85b6e9aaedea1d288338c/zstandard-0.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9c236e635582742fee16603042553d276cca506e824fa2e6489db04039521e90", size = 5655398, upload-time = "2024-07-15T00:16:06.694Z" }, + { url = "https://files.pythonhosted.org/packages/f2/61/ac78a1263bc83a5cf29e7458b77a568eda5a8f81980691bbc6eb6a0d45cc/zstandard-0.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a8fffdbd9d1408006baaf02f1068d7dd1f016c6bcb7538682622c556e7b68e35", size = 5191313, upload-time = "2024-07-15T00:16:09.758Z" }, + { url = "https://files.pythonhosted.org/packages/e7/54/967c478314e16af5baf849b6ee9d6ea724ae5b100eb506011f045d3d4e16/zstandard-0.23.0-cp312-cp312-win32.whl", hash = "sha256:dc1d33abb8a0d754ea4763bad944fd965d3d95b5baef6b121c0c9013eaf1907d", size = 430877, upload-time = "2024-07-15T00:16:11.758Z" }, + { url = "https://files.pythonhosted.org/packages/75/37/872d74bd7739639c4553bf94c84af7d54d8211b626b352bc57f0fd8d1e3f/zstandard-0.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:64585e1dba664dc67c7cdabd56c1e5685233fbb1fc1966cfba2a340ec0dfff7b", size = 495595, upload-time = "2024-07-15T00:16:13.731Z" }, + { url = "https://files.pythonhosted.org/packages/80/f1/8386f3f7c10261fe85fbc2c012fdb3d4db793b921c9abcc995d8da1b7a80/zstandard-0.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:576856e8594e6649aee06ddbfc738fec6a834f7c85bf7cadd1c53d4a58186ef9", size = 788975, upload-time = "2024-07-15T00:16:16.005Z" }, + { url = "https://files.pythonhosted.org/packages/16/e8/cbf01077550b3e5dc86089035ff8f6fbbb312bc0983757c2d1117ebba242/zstandard-0.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38302b78a850ff82656beaddeb0bb989a0322a8bbb1bf1ab10c17506681d772a", size = 633448, upload-time = "2024-07-15T00:16:17.897Z" }, + { url = "https://files.pythonhosted.org/packages/06/27/4a1b4c267c29a464a161aeb2589aff212b4db653a1d96bffe3598f3f0d22/zstandard-0.23.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2240ddc86b74966c34554c49d00eaafa8200a18d3a5b6ffbf7da63b11d74ee2", size = 4945269, upload-time = "2024-07-15T00:16:20.136Z" }, + { url = "https://files.pythonhosted.org/packages/7c/64/d99261cc57afd9ae65b707e38045ed8269fbdae73544fd2e4a4d50d0ed83/zstandard-0.23.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ef230a8fd217a2015bc91b74f6b3b7d6522ba48be29ad4ea0ca3a3775bf7dd5", size = 5306228, upload-time = "2024-07-15T00:16:23.398Z" }, + { url = "https://files.pythonhosted.org/packages/7a/cf/27b74c6f22541f0263016a0fd6369b1b7818941de639215c84e4e94b2a1c/zstandard-0.23.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:774d45b1fac1461f48698a9d4b5fa19a69d47ece02fa469825b442263f04021f", size = 5336891, upload-time = "2024-07-15T00:16:26.391Z" }, + { url = "https://files.pythonhosted.org/packages/fa/18/89ac62eac46b69948bf35fcd90d37103f38722968e2981f752d69081ec4d/zstandard-0.23.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f77fa49079891a4aab203d0b1744acc85577ed16d767b52fc089d83faf8d8ed", size = 5436310, upload-time = "2024-07-15T00:16:29.018Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a8/5ca5328ee568a873f5118d5b5f70d1f36c6387716efe2e369010289a5738/zstandard-0.23.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac184f87ff521f4840e6ea0b10c0ec90c6b1dcd0bad2f1e4a9a1b4fa177982ea", size = 4859912, upload-time = "2024-07-15T00:16:31.871Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ca/3781059c95fd0868658b1cf0440edd832b942f84ae60685d0cfdb808bca1/zstandard-0.23.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c363b53e257246a954ebc7c488304b5592b9c53fbe74d03bc1c64dda153fb847", size = 4936946, upload-time = "2024-07-15T00:16:34.593Z" }, + { url = "https://files.pythonhosted.org/packages/ce/11/41a58986f809532742c2b832c53b74ba0e0a5dae7e8ab4642bf5876f35de/zstandard-0.23.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e7792606d606c8df5277c32ccb58f29b9b8603bf83b48639b7aedf6df4fe8171", size = 5466994, upload-time = "2024-07-15T00:16:36.887Z" }, + { url = "https://files.pythonhosted.org/packages/83/e3/97d84fe95edd38d7053af05159465d298c8b20cebe9ccb3d26783faa9094/zstandard-0.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a0817825b900fcd43ac5d05b8b3079937073d2b1ff9cf89427590718b70dd840", size = 4848681, upload-time = "2024-07-15T00:16:39.709Z" }, + { url = "https://files.pythonhosted.org/packages/6e/99/cb1e63e931de15c88af26085e3f2d9af9ce53ccafac73b6e48418fd5a6e6/zstandard-0.23.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9da6bc32faac9a293ddfdcb9108d4b20416219461e4ec64dfea8383cac186690", size = 4694239, upload-time = "2024-07-15T00:16:41.83Z" }, + { url = "https://files.pythonhosted.org/packages/ab/50/b1e703016eebbc6501fc92f34db7b1c68e54e567ef39e6e59cf5fb6f2ec0/zstandard-0.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fd7699e8fd9969f455ef2926221e0233f81a2542921471382e77a9e2f2b57f4b", size = 5200149, upload-time = "2024-07-15T00:16:44.287Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e0/932388630aaba70197c78bdb10cce2c91fae01a7e553b76ce85471aec690/zstandard-0.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d477ed829077cd945b01fc3115edd132c47e6540ddcd96ca169facff28173057", size = 5655392, upload-time = "2024-07-15T00:16:46.423Z" }, + { url = "https://files.pythonhosted.org/packages/02/90/2633473864f67a15526324b007a9f96c96f56d5f32ef2a56cc12f9548723/zstandard-0.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ce8b52c5987b3e34d5674b0ab529a4602b632ebab0a93b07bfb4dfc8f8a33", size = 5191299, upload-time = "2024-07-15T00:16:49.053Z" }, + { url = "https://files.pythonhosted.org/packages/b0/4c/315ca5c32da7e2dc3455f3b2caee5c8c2246074a61aac6ec3378a97b7136/zstandard-0.23.0-cp313-cp313-win32.whl", hash = "sha256:a9b07268d0c3ca5c170a385a0ab9fb7fdd9f5fd866be004c4ea39e44edce47dd", size = 430862, upload-time = "2024-07-15T00:16:51.003Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bf/c6aaba098e2d04781e8f4f7c0ba3c7aa73d00e4c436bcc0cf059a66691d1/zstandard-0.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:f3513916e8c645d0610815c257cbfd3242adfd5c4cfa78be514e5a3ebb42a41b", size = 495578, upload-time = "2024-07-15T00:16:53.135Z" }, +]