From f77fcf8b602d0277af05544db6c97cb92c902406 Mon Sep 17 00:00:00 2001 From: alexsavio Date: Mon, 2 Jun 2025 23:07:00 +0200 Subject: [PATCH 1/4] feat!: upgrate to Python 3.12, clean up code, add type hints, replace pipenv for uv --- .editorconfig | 13 +- .github/workflows/api-quality.tpl.yml | 144 ++++ .github/workflows/deployment.tpl.yml | 43 ++ .github/workflows/main.yml | 25 + .github/workflows/pull_request.yml | 25 + .github/workflows/python-quality.tpl.yml | 124 ++++ .gitignore | 3 + .pre-commit-config.yaml | 37 - ci-scripts/bump-python-version.sh | 13 + ci-scripts/common.sh | 50 ++ ci-scripts/pypi-release.sh | 17 + docstamp/cli/cli.py | 181 +++-- docstamp/cli/utils.py | 20 +- docstamp/collections.py | 13 +- docstamp/commands.py | 17 +- docstamp/config.py | 115 +-- docstamp/data_source.py | 39 - docstamp/exceptions.py | 13 + docstamp/file_utils.py | 93 +-- docstamp/inkscape.py | 89 ++- docstamp/model.py | 76 -- docstamp/pdf_utils.py | 50 +- docstamp/pdflatex.py | 166 +++-- docstamp/qrcode.py | 83 ++- docstamp/svg_fonts.py | 141 ++-- docstamp/svg_utils.py | 100 +-- docstamp/template.py | 304 ++++---- docstamp/unicode_csv.py | 28 +- docstamp/vcard.py | 62 +- docstamp/version.py | 3 +- docstamp/xml_utils.py | 27 +- justfile | 89 +++ pyproject.toml | 164 +++++ scripts/embed_font_to_svg.py | 101 +-- scripts/svg_export.py | 49 +- setup.cfg | 26 +- setup.py | 5 - uv.lock | 859 +++++++++++++++++++++++ 38 files changed, 2519 insertions(+), 888 deletions(-) create mode 100644 .github/workflows/api-quality.tpl.yml create mode 100644 .github/workflows/deployment.tpl.yml create mode 100644 .github/workflows/main.yml create mode 100644 .github/workflows/pull_request.yml create mode 100644 .github/workflows/python-quality.tpl.yml delete mode 100644 .pre-commit-config.yaml create mode 100755 ci-scripts/bump-python-version.sh create mode 100755 ci-scripts/common.sh create mode 100755 ci-scripts/pypi-release.sh delete mode 100644 docstamp/data_source.py create mode 100644 docstamp/exceptions.py delete mode 100644 docstamp/model.py create mode 100644 justfile create mode 100644 pyproject.toml delete mode 100644 setup.py create mode 100644 uv.lock 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/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/cli.py b/docstamp/cli/cli.py index db9154c..470b75a 100644 --- a/docstamp/cli/cli.py +++ b/docstamp/cli/cli.py @@ -1,21 +1,20 @@ #!python -import click - -import os -import math import logging +import math +import os -from docstamp.file_utils import get_extension -from docstamp.template import TextDocument -from docstamp.config import LOGGING_LVL +import click from docstamp.cli.utils import ( CONTEXT_SETTINGS, - verbose_switch, - get_items_from_csv, + DirPath, ExistingFilePath, - DirPath + get_items_from_csv, + verbose_switch, ) +from docstamp.config import LOGGING_LVL +from docstamp.file_utils import get_extension +from docstamp.template import TextDocument ACCEPTED_DOCS = "Inkscape (.svg), PDFLatex (.tex), XeLatex (.tex)" @@ -27,40 +26,91 @@ def cli(): @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): +@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. @@ -78,12 +128,12 @@ def create(input, template, field, outdir, prefix, otype, command, index, fields = field # init set of template contents - log.debug('Reading CSV elements from {}.'.format(input_file)) + log.debug(f"Reading CSV elements from {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.') + click.echo("Quiting because found 0 items.") exit(-1) if not fields: @@ -93,24 +143,24 @@ def create(input, template, field, outdir, prefix, otype, command, index, # 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)) + raise ValueError( + f"Field name {field_name} not found in input file header." + ) # 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)) + log.debug(f"Using the elements with index {index} of the input file.") # 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)) + log.debug(f"Creating the template object using the file {template}.") template_doc = TextDocument.from_template_file(template, command) - log.debug('Created an object of type {}.'.format(type(template_doc))) + log.debug(f"Created an object of type {type(template_doc)}.") # let's stamp them! for idx in items: @@ -122,38 +172,35 @@ def create(input, template, field, outdir, prefix, otype, command, index, field_values = [] try: for field_name in fields: - field_values.append(item[field_name].replace(' ', '')) + field_values.append(item[field_name].replace(" ", "")) except: - log.exception('Could not get field {} value from' - ' {}'.format(field_name, item)) + log.exception(f"Could not get field {field_name} value from {item}") exit(-1) else: - file_name = '_'.join(field_values) + file_name = "_".join(field_values) - log.debug('Filling template {} with values of item {}.'.format(file_name, idx)) + log.debug(f"Filling template {file_name} with values of item {idx}.") try: template_doc.fill(item) except: - log.exception('Error filling document for {}th item'.format(idx)) + log.exception(f"Error filling document for {idx}th item") continue # set output file path file_extension = get_extension(template) if prefix is None: - basename = os.path.basename(template).replace(file_extension, '') + basename = os.path.basename(template).replace(file_extension, "") - file_name = basename + '_' + file_name - file_path = os.path.join(outdir, file_name + '.' + otype) + file_name = basename + "_" + file_name + file_path = os.path.join(outdir, file_name + "." + otype) - kwargs = {'file_type': otype, - 'dpi': dpi, - 'support_unicode': unicode_support} + kwargs = {"file_type": otype, "dpi": dpi, "support_unicode": unicode_support} - log.debug('Rendering file {}.'.format(file_path)) + log.debug(f"Rendering file {file_path}.") try: template_doc.render(file_path, **kwargs) except: - log.exception('Error creating {} for {}.'.format(file_path, item)) + log.exception(f"Error creating {file_path} for {item}.") exit(-1) else: - log.debug('Successfully rendered {}.'.format(file_path)) + log.debug(f"Successfully rendered {file_path}.") diff --git a/docstamp/cli/utils.py b/docstamp/cli/utils.py index 51e52f3..6c1b134 100644 --- a/docstamp/cli/utils.py +++ b/docstamp/cli/utils.py @@ -1,10 +1,10 @@ -# -*- coding: utf-8 -*- """ Utilities for the CLI functions. """ -import re + import json import logging +import re from csv import DictReader import click @@ -12,9 +12,8 @@ from docstamp.model import json_to_dict # different context options -CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) -UNKNOWN_OPTIONS = dict(allow_extra_args=True, - ignore_unknown_options=True) +CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) +UNKNOWN_OPTIONS = dict(allow_extra_args=True, ignore_unknown_options=True) # specification of existing ParamTypes DirPath = click.Path(file_okay=False, resolve_path=True) @@ -26,19 +25,19 @@ # validators def check_not_none(ctx, param, value): if value is None: - raise click.BadParameter('got {}.'.format(value)) + raise click.BadParameter(f"got {value}.") return value # declare custom click.ParamType class RegularExpression(click.ParamType): - name = 'regex' + name = "regex" def convert(self, value, param, ctx): try: rex = re.compile(value, re.IGNORECASE) except ValueError: - self.fail('%s is not a valid regular expression.' % value, param, ctx) + self.fail("%s is not a valid regular expression." % value, param, ctx) else: return rex @@ -53,13 +52,12 @@ def get_items_from_csv(csv_filepath): # 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]): + if any([item[i] != "" for i in item]): items[idx] = item return items, reader.fieldnames diff --git a/docstamp/collections.py b/docstamp/collections.py index 83b7ebc..6f971ee 100644 --- a/docstamp/collections.py +++ b/docstamp/collections.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import pickle @@ -10,8 +8,7 @@ def __getattr__(self, name): raise AttributeError -class ItemSet(object): - +class ItemSet: def __iter__(self): return self.items.__iter__() @@ -22,20 +19,20 @@ def next(self): return self.items.next() def __getitem__(self, item): - if hasattr(self.items, '__getitem__'): + if hasattr(self.items, "__getitem__"): return self.items[item] else: - raise AttributeError('Item set has no __getitem__ implemented.') + 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'): + 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'): + 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..e07cb61 100644 --- a/docstamp/commands.py +++ b/docstamp/commands.py @@ -1,4 +1,3 @@ -# coding=utf-8 # ------------------------------------------------------------------------------- # Author: Alexandre Manhaes Savio # Grupo de Inteligencia Computational @@ -8,18 +7,18 @@ # Use this at your own risk! # ------------------------------------------------------------------------------- +import logging import os -import sys import shutil -import logging import subprocess +import sys from subprocess import CalledProcessError log = logging.getLogger(__name__) def simple_call(cmd_args): - return subprocess.call(' '.join(cmd_args), shell=True) + return subprocess.call(" ".join(cmd_args), shell=True) def is_exe(fpath): @@ -66,11 +65,11 @@ def which_py2(cmd_name): def check_command(cmd_name): - """ Raise a FileNotFoundError if the command is not found. + """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)) + raise FileNotFoundError(f"Could not find command named {cmd_name}.") def call_command(cmd_name, args_strings): @@ -96,13 +95,13 @@ def call_command(cmd_name, args_strings): try: cmd_line = [cmd_fullpath] + args_strings - log.debug('Calling: `{}`.'.format(' '.join(cmd_line))) + log.debug("Calling: `{}`.".format(" ".join(cmd_line))) # retval = subprocess.check_call(cmd_line) - retval = subprocess.call(' '.join(cmd_line), shell=True) + retval = subprocess.call(" ".join(cmd_line), shell=True) except CalledProcessError as ce: log.exception( "Error calling command with arguments: " - "{} \n With return code: {}".format(cmd_line, ce.returncode) + f"{cmd_line} \n With return code: {ce.returncode}" ) raise else: diff --git a/docstamp/config.py b/docstamp/config.py index 27b5da5..9908053 100644 --- a/docstamp/config.py +++ b/docstamp/config.py @@ -1,25 +1,12 @@ -# 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 os import re -import logging +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) +from docstamp.commands import is_exe, which -def find_file_match(folder_path, regex=''): +def find_file_match(folder_path, regex=""): """ Returns absolute paths of files that match the regex within folder_path and all its children folders. @@ -40,44 +27,41 @@ 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)]) + outlist.extend([os.path.join(root, f) for f in files if re.match(regex, f)]) return outlist def get_system_path(): - if _platform == "linux" or _platform == "linux2": - return os.environ['PATH'] - elif _platform == "darwin": - return os.environ['PATH'] + if _platform == "linux" or _platform == "linux2" or _platform == "darwin": + return os.environ["PATH"] elif _platform == "win32": # don't know if this works - return os.environ['PATH'] + return os.environ["PATH"] def get_other_program_folders(): if _platform == "linux" or _platform == "linux2": - return ['/opt/bin'] + return ["/opt/bin"] elif _platform == "darwin": - return ['/Applications', os.path.join(os.environ['HOME'], 'Applications')] + return ["/Applications", os.path.join(os.environ["HOME"], "Applications")] elif _platform == "win32": # don't know if this works - return ['C:\Program Files'] + return [r"C:\Program Files"] def get_temp_dir(): if _platform == "linux" or _platform == "linux2": - return '/tmp' + return "/tmp" elif _platform == "darwin": - return '.' + return "." elif _platform == "win32": # don't know if this works return None def find_in_other_programs_folders(app_name): - app_name_regex = '^' + app_name + '$' + app_name_regex = "^" + app_name + "$" other_folders = get_other_program_folders() for folder in other_folders: @@ -96,27 +80,57 @@ def find_program(root_dir, exec_name): return None -def ask_for_path_of(app_name): +def ask_for_path_of(app_name: str) -> str: + """Ask the user for the path of the application binary. + This function will repeatedly prompt the user until a valid executable + file is provided. + Parameters + ---------- + app_name: str + The name of the application to find. + Returns + ------- + str + The path to the binary file. + Raises + ------ + ValueError + If the provided path does not exist or is not executable. + """ 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)) + bin_path = input( + f"Insert path of {app_name} executable file [Press Ctrl+C to exit]: " + ) if not os.path.exists(bin_path): - print('Could not find file {}. Try it again.'.format(bin_path)) + print(f"Could not find file {bin_path}. Try it again.") bin_path = None continue if not is_exe(bin_path): - print('No execution permissions on file {}. Try again.'.format(bin_path)) + print(f"No execution permissions on file {bin_path}. Try again.") bin_path = None continue return bin_path -def proactive_search_of(app_name): - if _platform == 'win32': - bin_name = app_name + '.exe' +def proactive_search_of(app_name: str) -> str | None: + """Proactively search for the binary of the given application. + This function checks the system PATH, common program folders, and prompts + the user for the path if the binary is not found. + Parameters + ---------- + app_name: str + The name of the application to search for. + Returns + ------- + str | None + The path to the binary if found, otherwise None. + """ + if _platform == "win32": + bin_name = app_name + ".exe" else: bin_name = app_name @@ -131,29 +145,22 @@ def proactive_search_of(app_name): return ask_for_path_of(bin_name) -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' + bin_name = "inkscape-bin" - if 'INKSCAPE_BINPATH' not in globals(): + if "INKSCAPE_BINPATH" not in globals(): global INKSCAPE_BINPATH INKSCAPE_BINPATH = proactive_search_of(bin_name) - return INKSCAPE_BINPATH + return Path(INKSCAPE_BINPATH) if INKSCAPE_BINPATH else None -def get_lyx_binpath(): - if 'LYX_BINPATH' not in globals(): +def get_lyx_binpath() -> Path | None: + """Return the LyX binary path.""" + 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') - -# JINJA_ENV = Environment(loader=PackageLoader('docstamp', 'templates')) -# JINJA_ENV = Environment(loader=FileSystemLoader(TEMPLATES_DIR)) - -# FILE_EXPORTERS = {'.svg': Inkscape,} -# '.tex': PdfLatex, -# '.lyx': LyX} + LYX_BINPATH = proactive_search_of("lyx") + return Path(LYX_BINPATH) if LYX_BINPATH else None 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..e924bcf --- /dev/null +++ b/docstamp/exceptions.py @@ -0,0 +1,13 @@ +"""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.""" diff --git a/docstamp/file_utils.py b/docstamp/file_utils.py index 72932b9..be05568 100644 --- a/docstamp/file_utils.py +++ b/docstamp/file_utils.py @@ -1,22 +1,12 @@ -# 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 -log = logging.getLogger(__name__) - def get_extension(filepath, check_if_exists=False): """Return the extension of fpath. @@ -35,9 +25,9 @@ def get_extension(filepath, check_if_exists=False): """ if check_if_exists: if not os.path.exists(filepath): - err = 'File not found: ' + filepath + err = "File not found: " + filepath log.error(err) - raise IOError(err) + raise FileNotFoundError(err) try: rest, ext = os.path.splitext(filepath) @@ -69,9 +59,9 @@ def add_extension_if_needed(filepath, ext, check_if_exists=False): if check_if_exists: if not os.path.exists(filepath): - err = 'File not found: ' + filepath + err = "File not found: " + filepath log.error(err) - raise IOError(err) + raise OSError(err) return filepath @@ -89,11 +79,11 @@ def remove_ext(filepath): str File path or name without extension """ - return filepath[:filepath.rindex(get_extension(filepath))] + return filepath[: filepath.rindex(get_extension(filepath))] -def get_tempfile(suffix='.txt', dirpath=None): - """ Return a temporary file with the given suffix within dirpath. +def get_tempfile(suffix=".txt", dirpath=None): + """Return a temporary file with the given suffix within dirpath. If dirpath is None, will look for a temporary folder in your system. Parameters @@ -116,7 +106,7 @@ def get_tempfile(suffix='.txt', dirpath=None): def cleanup(workdir, extension): - """ Remove the files in workdir that have the given extension. + """Remove the files in workdir that have the given extension. Parameters ---------- @@ -126,17 +116,11 @@ 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) + [os.remove(f) for f in glob(os.path.join(workdir, "*." + extension))] 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`. + """Convert a CSV file in `csv_filepath` into a JSON file in `json_filepath`. Parameters ---------- @@ -154,8 +138,8 @@ def csv_to_json(csv_filepath, json_filepath, fieldnames, ignore_first_line=True) import csv import json - csvfile = open(csv_filepath, 'r') - jsonfile = open(json_filepath, 'w') + csvfile = open(csv_filepath) + jsonfile = open(json_filepath, "w") reader = csv.DictReader(csvfile, fieldnames) rows = [] @@ -170,33 +154,8 @@ def csv_to_json(csv_filepath, json_filepath, fieldnames, ignore_first_line=True) 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 +171,18 @@ 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() - - content = content.replace(old, new, max) - with open(filepath, 'w') as f: - f.write(content) + _filepath = Path(filepath) + content = _filepath.read_text() + content = content.replace(old=old, new=new, count=max) + _filepath.write_text(content) -def cleanup_docstamp_output(output_dir=''): - """ Remove the 'tmp*.aux', 'tmp*.out' and 'tmp*.log' files in `output_dir`. +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)))] + suffixes = ["aux", "out", "log"] + files = [ + f for suf in suffixes for f in glob(os.path.join(output_dir, f"tmp*.{suf}")) + ] [os.remove(file) for file in files] diff --git a/docstamp/inkscape.py b/docstamp/inkscape.py index a72f9ac..9c34140 100644 --- a/docstamp/inkscape.py +++ b/docstamp/inkscape.py @@ -1,24 +1,20 @@ -# 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 +import os -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 +): """Call inkscape CLI with arguments and returns its return value. Parameters @@ -32,21 +28,23 @@ 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.') + 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.' + raise FileNotFoundError( + "Inkscape binary has not been found. Please check configuration." ) return call_command(inkscape_binpath, 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, output_file, export_flag="-A", dpi=90, inkscape_binpath=None +): + """Call Inkscape to export the input_file to output_file using the specific export argument flag for the output file type. Parameters @@ -68,35 +66,48 @@ def inkscape_export(input_file, output_file, export_flag="-A", dpi=90, inkscape_ """ if not os.path.exists(input_file): - log.error('File {} not found.'.format(input_file)) - raise IOError((0, 'File not found.', input_file)) + log.error(f"File {input_file} not found.") + raise FileNotFoundError((0, "File not found.", input_file)) - if '=' not in export_flag: - export_flag += ' ' + 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)] + arg_strings += ["--without-gui"] + arg_strings += ["--export-text-to-path"] + arg_strings += ["--export-pdf-version=1.5"] + arg_strings += [f'{export_flag}"{output_file}"'] + arg_strings += [f"--export-dpi={dpi}"] + arg_strings += [f'"{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 - """ +def svg2pdf( + svg_file_path, pdf_file_path, dpi=150, command_binpath=None, support_unicode=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( + svg_file_path, 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( + svg_file_path, + pdf_file_path, + export_flag="-A", + dpi=dpi, + inkscape_binpath=command_binpath, + ) + + +def svg2png(svg_file_path, png_file_path, dpi=150, inkscape_binpath=None) -> int: + """Transform SVG file to PNG file""" + return inkscape_export( + svg_file_path, + 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..2eebebd 100644 --- a/docstamp/pdf_utils.py +++ b/docstamp/pdf_utils.py @@ -1,13 +1,17 @@ -""" -Function helpers to manage PDF files. -""" +"""Function helpers to manage PDF files.""" + +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 +28,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"))) + merger.write(str(out_filepath)) return 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..b757462 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 check_command, 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)) + tex_file_path = Path(tex_file) + output_file_path = Path(output_file) if output_file else None + _check_latex_file_inputs( + tex_file=tex_file_path, + output_file=output_file_path, + output_format=output_format, + ) - cmd_name = 'pdflatex' - check_command(cmd_name) + cmd_name = "pdflatex" + check_command(cmd_name=cmd_name) args_strings = [cmd_name] + result_dir = "" 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) + output_dir = output_file_path.parent.absolute() + args_strings += [f'-output-directory="{output_dir}"'] + result_dir = output_file_path.parent + else: + result_dir = tex_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) + tex_file_name = f"{remove_ext(tex_file_path.name)}.{output_format}" + result_file = result_dir / tex_file_name + if result_file.exists(): + shutil.move(result_file, output_file_path) else: - raise IOError('Could not find PDFLatex result file.') + 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') + _cleanup_aux_log_files(workdir=result_dir) + return exit_code - return ret - -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,42 @@ 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)) + tex_file_path = Path(tex_file) + output_file_path = Path(output_file) if output_file else None + _check_latex_file_inputs( + tex_file=tex_file_path, + output_file=output_file_path, + output_format=output_format, + ) - cmd_name = 'xelatex' - check_command(cmd_name) + cmd_name = "xelatex" + check_command(cmd_name=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)))] + output_dir = output_file_path.parent.absolute() + args_strings += [f'-output-directory="{output_dir}"'] + result_dir = output_file_path.parent + else: + result_dir = tex_file_path.parent - if output_format == 'dvi': - args_strings += ['-no-pdf'] + if output_format == "dvi": + args_strings += ["-no-pdf"] - result_dir = os.path.dirname(output_file) if output_file else os.path.dirname(tex_file) - args_strings += ['"' + tex_file + '"'] + 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)) + '.pdf') - if os.path.exists(result_file): - shutil.move(result_file, output_file) + tex_file_name = f"{remove_ext(tex_file_path.name)}.{output_format}" + result_file = result_dir / tex_file_name + if result_file.exists(): + shutil.move(result_file, output_file_path) else: - raise IOError('Could not find PDFLatex result file.') + raise FileNotFoundError("Could not find XeLatex result file.") - 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..daa5b83 100644 --- a/docstamp/qrcode.py +++ b/docstamp/qrcode.py @@ -1,14 +1,60 @@ -""" -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. + 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.add_data(text) + qr.make(fit=True) + return qr.make_image(image_factory=qrcode.image.svg.SvgPathImage) + except Exception as exc: + raise QRCodeError( + f"Error trying to generate QR code from `vcard_string`: {text}" + ) from exc + + +def save_into_qrcode( + text: str, + out_filepath: os.PathLike | str, + color: str = "", + box_size: float = 10, +): + """Save `text` in a qrcode svg image file. Parameters ---------- @@ -24,25 +70,14 @@ def save_into_qrcode(text, out_filepath, color='', box_size=10, pixel_size=1850) box_size: scalar Size of the QR code boxes. """ - try: - 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) - 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) - + img = _create_qrcode_image(text=text, box_size=box_size) + _ = _save_qrcode(qrcode=img, out_filepath=out_filepath) if color: - replace_file_content(out_filepath, 'fill:#000000', 'fill:#{}'.format(color)) + replace_file_content(out_filepath, "fill:#000000", f"fill:#{color}") -def _qrcode_to_file(qrcode, out_filepath): - """ Save a `qrcode` object into `out_filepath`. +def _save_qrcode(qrcode: qrcode.image.svg.SvgPathImage, out_filepath: str): + """Save a `qrcode` object into `out_filepath`. Parameters ---------- qrcode: qrcode object @@ -53,6 +88,8 @@ def _qrcode_to_file(qrcode, out_filepath): try: qrcode.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 diff --git a/docstamp/svg_fonts.py b/docstamp/svg_fonts.py index 660fe3c..e015ef6 100644 --- a/docstamp/svg_fonts.py +++ b/docstamp/svg_fonts.py @@ -1,104 +1,141 @@ -# 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 = {"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().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)] + return FONT_TYPES[get_extension(filepath=self.filepath)] else: return 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{\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): - 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 +146,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 +154,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 +174,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..230a2c0 100644 --- a/docstamp/svg_utils.py +++ b/docstamp/svg_utils.py @@ -1,17 +1,18 @@ -""" -Function helpers to do stuff on svg files. -""" +"""Function helpers to do stuff on svg files.""" + +from __future__ import annotations + import os -import logging +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, check_command, which -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 +25,10 @@ def replace_chars_for_svg_code(svg_content): """ result = svg_content svg_char = [ - ('&', '&'), - ('>', '>'), - ('<', '<'), - ('"', '"'), + ("&", "&"), + (">", ">"), + ("<", "<"), + ('"', """), ] for c, entity in svg_char: @@ -36,8 +37,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 +59,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 +101,58 @@ 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: str | Path, + dpi: int = 90, + rsvg_binpath: 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 Path to the input file output_file: str Path to the output file + dpi: int + Dots per inch for the output file. Default is 90. + + rsvg_binpath: str + Path to `rsvg-convert` command + 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] + rsvg_binpath = which(cmd_name="rsvg-convert") + check_command(cmd_name=rsvg_binpath) + + args_strings = [ + "-f pdf", + f"-o {output_file}", + f"--dpi-x {dpi}", + f"--dpi-y {dpi}", + 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..71d7aa4 100644 --- a/docstamp/template.py +++ b/docstamp/template.py @@ -1,128 +1,92 @@ -# 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"], +) -> type[TextDocument]: + 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)) - + raise ValueError( + f"Could not determine the document type for `extension` {extension}." + ) return doc_type -def get_doctype_by_command(command): +def get_doctype_by_command( + command: Literal["inkscape", "pdflatex", "xelatex"] | None, +) -> type[TextDocument]: 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)) + raise ValueError( + f"Could not determine the document type for `command` {command}." + ) 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) - - if doc_contents is not None: - self.file_content_ = self.fill(doc_contents) + def __init__( + self, + template_file_path: os.PathLike | str, + ): + self._template_file = Path(template_file_path) - def _setup_template_file(self, template_file_path): - """ Setup self.template + if not self._template_file.exists(): + raise FileNotFoundError( + f"Could not find template file {template_file_path}." + ) - 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 + self._template_env = Environment( + loader=FileSystemLoader(self._template_file.parent), + autoescape=False, # noqa: S701 + ) + self.template = self._template_env.get_template(template_file_path.name) - 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 +94,42 @@ 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], + encoding: str = "utf-8", + ): + """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) 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=None): + """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,7 +146,7 @@ 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) @@ -198,11 +159,10 @@ class given by the `command` or the extension of `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]) -> 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. @@ -220,18 +180,31 @@ def fill(self, 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) + 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 render(self, file_path, **kwargs): - """ Save the content of the .svg file in the chosen rendered format. + 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 +215,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 +298,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 index 2c1cab0..dd18d60 100644 --- a/docstamp/unicode_csv.py +++ b/docstamp/unicode_csv.py @@ -1,12 +1,12 @@ -# -*- coding: utf-8 -*- +from __future__ import annotations -import csv import codecs +import csv +from io import StringIO +from typing import TYPE_CHECKING -try: - from cStringIO import StringIO -except: - from io import StringIO +if TYPE_CHECKING: + from csv import Dialect class UnicodeWriter: @@ -15,14 +15,20 @@ class UnicodeWriter: which is encoded in the given encoding. """ - def __init__(self, f, dialect=csv.excel, encoding="utf-8", **kwds): + def __init__( + self, + file: StringIO, + dialect: Dialect = csv.excel, + encoding: str = "utf-8", + **kwargs, + ): # Redirect output to a queue self.queue = StringIO() - self.writer = csv.writer(self.queue, dialect=dialect, **kwds) - self.stream = f + self.writer = csv.writer(self.queue, dialect=dialect, **kwargs) + self.stream = file self.encoder = codecs.getincrementalencoder(encoding)() - def writerow(self, row): + def writerow(self, row: list[str]): self.writer.writerow([s.encode("utf-8") for s in row]) # Fetch UTF-8 output from the queue ... data = self.queue.getvalue() @@ -34,6 +40,6 @@ def writerow(self, row): # empty queue self.queue.truncate(0) - def writerows(self, rows): + def writerows(self, rows: list[list[str]]): 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..cd1ee63 100644 --- a/docstamp/version.py +++ b/docstamp/version.py @@ -1,2 +1 @@ -"""Release version number.""" -__version__ = '0.4.5' # noqa +__version__ = "0.4.4" 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..6173eb1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,164 @@ +[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", +] +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.cli: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..eb277aa --- /dev/null +++ b/uv.lock @@ -0,0 +1,859 @@ +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 = "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 = "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 = "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" }, +] From 1c1e9b27f2efeb511d01e7cda528386a473d4ace Mon Sep 17 00:00:00 2001 From: alexsavio Date: Wed, 18 Jun 2025 09:32:43 +0200 Subject: [PATCH 2/4] fix --- CHANGES.rst | 7 +++ docstamp/cli/cli.py | 85 +++++++++++++++++------------ docstamp/cli/utils.py | 59 ++++++++------------ docstamp/collections.py | 38 ------------- docstamp/commands.py | 84 +++++++---------------------- docstamp/config.py | 83 ++++++++++++++++------------ docstamp/exceptions.py | 4 ++ docstamp/file_utils.py | 116 ++++++++-------------------------------- docstamp/inkscape.py | 62 +++++++++++++-------- docstamp/pdf_utils.py | 7 ++- docstamp/qrcode.py | 21 ++------ docstamp/svg_utils.py | 7 +-- docstamp/template.py | 8 ++- docstamp/unicode_csv.py | 45 ---------------- docstamp/version.py | 2 +- pyproject.toml | 1 + uv.lock | 40 ++++++++++++++ 17 files changed, 276 insertions(+), 393 deletions(-) delete mode 100644 docstamp/collections.py delete mode 100644 docstamp/unicode_csv.py 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/docstamp/cli/cli.py b/docstamp/cli/cli.py index 470b75a..dc9e923 100644 --- a/docstamp/cli/cli.py +++ b/docstamp/cli/cli.py @@ -1,7 +1,11 @@ #!python +from __future__ import annotations + import logging import math import os +from pathlib import Path +import sys import click @@ -98,7 +102,7 @@ def cli(): default=False, help="Allows unicode characters to be correctly encoded in the PDF.", ) -def create( +def create( # noqa: C901, PLR0912, PLR0913, PLR0915 input, template, field, @@ -122,85 +126,98 @@ def create( log = logging.getLogger(__name__) # setup verbose mode - verbose_switch(verbose) + 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(f"Reading CSV elements from {input_file}.") - items, fieldnames = get_items_from_csv(input_file) + 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.") - exit(-1) + sys.exit(-1) if not fields: - # set the number of zeros that the files will have + # 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 fieldnames: + if field_name not in header_fields: raise ValueError( - f"Field name {field_name} not found in input file header." + f"Field name {field_name} not found in input file header." ) # filter the items if index if index: - myitems = {idx: items[idx] for idx in index} - items = myitems - log.debug(f"Using the elements with index {index} of the input file.") + 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 - if not os.path.exists(outdir): - os.mkdir(outdir) + output_directory = Path(outdir) + output_directory.mkdir(parents=True, exist_ok=True) # create template document model - log.debug(f"Creating the template object using the file {template}.") + log.debug("Creating the template object using the file %s.", template) template_doc = TextDocument.from_template_file(template, command) - log.debug(f"Created an object of type {type(template_doc)}.") + log.debug("Created an object of type %s.", type(template_doc)) # let's stamp them! for idx in items: item = items[idx] - if not len(fields): + if not 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(f"Could not get field {field_name} value from {item}") - exit(-1) + except KeyError as _: + log.exception("Could not get field %s value from %s", field_name, item) + sys.exit(-1) else: file_name = "_".join(field_values) - log.debug(f"Filling template {file_name} with values of item {idx}.") + log.debug("Filling template %s with values of item %s.", file_name, idx) try: - template_doc.fill(item) + template_doc.render(item) except: - log.exception(f"Error filling document for {idx}th item") + log.exception("Error filling document for %sth item", 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} + file_extension = get_extension(template) + basename = Path(template).name.replace(file_extension, "") + else: + basename = prefix - log.debug(f"Rendering file {file_path}.") + file_path = output_directory / f"{basename + "_" + file_name}.{otype}" + log.debug("Rendering file %s.", file_path) try: - template_doc.render(file_path, **kwargs) + template_doc.export( + file_path=file_path, + file_type=otype, + dpi=dpi, + support_unicode=unicode_support, + ) except: - log.exception(f"Error creating {file_path} for {item}.") - exit(-1) + log.exception("Error creating %s for %s.", file_path, item) + sys.exit(-1) else: - log.debug(f"Successfully rendered {file_path}.") + log.debug("Successfully rendered %s.", file_path) diff --git a/docstamp/cli/utils.py b/docstamp/cli/utils.py index 6c1b134..9157f1e 100644 --- a/docstamp/cli/utils.py +++ b/docstamp/cli/utils.py @@ -2,18 +2,22 @@ Utilities for the CLI functions. """ -import json -import logging +from __future__ import annotations + +import os import re from csv import DictReader +from typing import TYPE_CHECKING import click +import orjson -from docstamp.model import json_to_dict +if TYPE_CHECKING: + 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) @@ -22,33 +26,25 @@ UnexistingFilePath = click.Path(dir_okay=False, resolve_path=True) -# validators -def check_not_none(ctx, param, value): - if value is None: - raise click.BadParameter(f"got {value}.") - return value - - -# declare custom click.ParamType class RegularExpression(click.ParamType): + """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: 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], list[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 = {} @@ -56,17 +52,8 @@ def get_items_from_csv(csv_filepath): 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 6f971ee..0000000 --- a/docstamp/collections.py +++ /dev/null @@ -1,38 +0,0 @@ -import pickle - - -class Enum(set): - def __getattr__(self, name): - if name in self: - return name - raise AttributeError - - -class ItemSet: - 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 e07cb61..dcaebbf 100644 --- a/docstamp/commands.py +++ b/docstamp/commands.py @@ -1,78 +1,28 @@ -# ------------------------------------------------------------------------------- -# 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 shutil import subprocess -import sys +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 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 check_command(cmd_name): """Raise a FileNotFoundError if the command is not found. :param cmd_name: """ - if which(cmd_name) is None: + if shutil.which(cmd_name) is None: raise FileNotFoundError(f"Could not find command named {cmd_name}.") -def call_command(cmd_name, args_strings): +def call_command(cmd_name: str, args_strings: list[str]) -> int: """Call CLI command with arguments and returns its return value. Parameters @@ -88,20 +38,22 @@ 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) + if not cmd_path.is_absolute(): + cmd_fullpath = shutil.which(cmd_name) else: cmd_fullpath = cmd_name 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 = [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: " - f"{cmd_line} \n With return code: {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 9908053..a99c5b3 100644 --- a/docstamp/config.py +++ b/docstamp/config.py @@ -1,12 +1,13 @@ +from __future__ import annotations + import os import re +import shutil from pathlib import Path from sys import platform as _platform -from docstamp.commands import is_exe, which - -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. @@ -16,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 @@ -26,28 +28,27 @@ 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(): - if _platform == "linux" or _platform == "linux2" or _platform == "darwin": - return os.environ["PATH"] - elif _platform == "win32": - # don't know if this works - return os.environ["PATH"] - - -def get_other_program_folders(): +def get_other_program_folders() -> list[Path]: if _platform == "linux" or _platform == "linux2": - return ["/opt/bin"] + return [ + Path("/opt/bin"), + Path("/usr/local/bin"), + Path("/usr/bin"), + Path("/bin"), + Path("/usr/sbin"), + Path("/sbin"), + ] elif _platform == "darwin": - return ["/Applications", os.path.join(os.environ["HOME"], "Applications")] + return [Path("/Applications"), Path(os.environ["HOME"]) / "Applications"] elif _platform == "win32": # don't know if this works - return [r"C:\Program Files"] + return Path(r"C:\Program Files") def get_temp_dir(): @@ -60,7 +61,7 @@ def get_temp_dir(): return None -def find_in_other_programs_folders(app_name): +def find_in_other_programs_folders(app_name: str) -> Path | None: app_name_regex = "^" + app_name + "$" other_folders = get_other_program_folders() @@ -72,15 +73,27 @@ 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: str) -> str: +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 == "win32": + return filepath.suffix.lower() in (".exe", ".bat", ".cmd") + else: + return filepath.is_file() and os.access(filepath, os.X_OK) + + +def ask_for_path_of(app_name: str) -> Path | None: """Ask the user for the path of the application binary. This function will repeatedly prompt the user until a valid executable file is provided. @@ -103,20 +116,20 @@ def ask_for_path_of(app_name: str) -> str: f"Insert path of {app_name} executable file [Press Ctrl+C to exit]: " ) - if not os.path.exists(bin_path): + if not Path(bin_path).exists(): print(f"Could not find file {bin_path}. Try it again.") bin_path = None continue - if not is_exe(bin_path): + if not is_executable(bin_path): print(f"No execution permissions on file {bin_path}. Try again.") bin_path = None continue - return bin_path + return Path(bin_path) if bin_path else None -def proactive_search_of(app_name: str) -> str | None: +def proactive_search_of(app_name: str) -> Path | None: """Proactively search for the binary of the given application. This function checks the system PATH, common program folders, and prompts the user for the path if the binary is not found. @@ -126,7 +139,7 @@ def proactive_search_of(app_name: str) -> str | None: The name of the application to search for. Returns ------- - str | None + Path | None The path to the binary if found, otherwise None. """ if _platform == "win32": @@ -134,13 +147,13 @@ def proactive_search_of(app_name: str) -> str | None: 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) @@ -155,7 +168,7 @@ def get_inkscape_binpath() -> Path | None: global INKSCAPE_BINPATH INKSCAPE_BINPATH = proactive_search_of(bin_name) - return Path(INKSCAPE_BINPATH) if INKSCAPE_BINPATH else None + return INKSCAPE_BINPATH def get_lyx_binpath() -> Path | None: @@ -163,4 +176,4 @@ def get_lyx_binpath() -> Path | None: if "LYX_BINPATH" not in globals(): global LYX_BINPATH LYX_BINPATH = proactive_search_of("lyx") - return Path(LYX_BINPATH) if LYX_BINPATH else None + return LYX_BINPATH diff --git a/docstamp/exceptions.py b/docstamp/exceptions.py index e924bcf..5cbf848 100644 --- a/docstamp/exceptions.py +++ b/docstamp/exceptions.py @@ -11,3 +11,7 @@ class ExportError(Exception): 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 be05568..149062b 100644 --- a/docstamp/file_utils.py +++ b/docstamp/file_utils.py @@ -2,71 +2,34 @@ import os import tempfile -from glob import glob from pathlib import Path from docstamp.config import get_temp_dir +from docstamp.exceptions import FileDeletionError -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 FileNotFoundError(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 OSError(err) - - return filepath - - -def remove_ext(filepath): +def remove_ext(filepath: os.PathLike | str) -> str: """Removes the extension of the file. Parameters @@ -79,10 +42,16 @@ 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 filepath + return filepath.removesuffix(extension) -def get_tempfile(suffix=".txt", dirpath=None): +def get_tempfile( + suffix: str = ".txt", + dirpath: os.PathLike | str | None = None, +) -> tempfile.NamedTemporaryFile: """Return a temporary file with the given suffix within dirpath. If dirpath is None, will look for a temporary folder in your system. @@ -105,7 +74,7 @@ def get_tempfile(suffix=".txt", dirpath=None): return tempfile.NamedTemporaryFile(suffix=suffix, dir=dirpath) -def cleanup(workdir, extension): +def cleanup(workdir: os.PathLike | str, extension: str): """Remove the files in workdir that have the given extension. Parameters @@ -116,42 +85,14 @@ 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 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) - jsonfile = open(json_filepath, "w") - - 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() + 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 def replace_file_content(filepath: os.PathLike | str, old: str, new: str, max: int = 1): @@ -175,14 +116,3 @@ def replace_file_content(filepath: os.PathLike | str, old: str, new: str, max: i content = _filepath.read_text() content = content.replace(old=old, new=new, count=max) _filepath.write_text(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, f"tmp*.{suf}")) - ] - [os.remove(file) for file in files] diff --git a/docstamp/inkscape.py b/docstamp/inkscape.py index 9c34140..d34442b 100644 --- a/docstamp/inkscape.py +++ b/docstamp/inkscape.py @@ -2,19 +2,18 @@ from __future__ import annotations -import logging import os +from pathlib import Path 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: list[str], inkscape_binpath: os.PathLike | str | None = None -): + args_strings: list[str], + inkscape_binpath: os.PathLike | str | None = None, +) -> int: """Call inkscape CLI with arguments and returns its return value. Parameters @@ -33,17 +32,21 @@ def call_inkscape( if inkscape_binpath is None: inkscape_binpath = get_inkscape_binpath() - if inkscape_binpath is None or not os.path.exists(inkscape_binpath): + if inkscape_binpath is None or not Path.exists(inkscape_binpath): 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 -): + 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. @@ -59,15 +62,21 @@ def inkscape_export( 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(f"File {input_file} not found.") - raise FileNotFoundError((0, "File not found.", input_file)) + if not Path.exists(input_file): + raise FileNotFoundError(f"File {input_file} not found.") if "=" not in export_flag: export_flag += " " @@ -79,34 +88,45 @@ def inkscape_export( arg_strings += [f'{export_flag}"{output_file}"'] arg_strings += [f"--export-dpi={dpi}"] arg_strings += [f'"{input_file}"'] - - return call_inkscape(arg_strings, inkscape_binpath=inkscape_binpath) + return call_inkscape(arg_strings=arg_strings, inkscape_binpath=inkscape_binpath) def svg2pdf( - svg_file_path, pdf_file_path, dpi=150, command_binpath=None, support_unicode=False + 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 + input_file=svg_file_path, + output_file=pdf_file_path, + dpi=dpi, + rsvg_binpath=command_binpath, ) return inkscape_export( - svg_file_path, - pdf_file_path, + input_file=svg_file_path, + output_file=pdf_file_path, export_flag="-A", dpi=dpi, inkscape_binpath=command_binpath, ) -def svg2png(svg_file_path, png_file_path, dpi=150, inkscape_binpath=None) -> int: +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( - svg_file_path, - png_file_path, + intput_file=svg_file_path, + output_file=png_file_path, export_flag="-e", dpi=dpi, inkscape_binpath=inkscape_binpath, diff --git a/docstamp/pdf_utils.py b/docstamp/pdf_utils.py index 2eebebd..873b87a 100644 --- a/docstamp/pdf_utils.py +++ b/docstamp/pdf_utils.py @@ -1,5 +1,7 @@ """Function helpers to manage PDF files.""" +from __future__ import annotations + import os from pathlib import Path @@ -9,7 +11,8 @@ def merge_pdfs( - pdf_filepaths: list[os.Pathlike | str], out_filepath: str | os.PathLike + 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`. @@ -35,7 +38,7 @@ def merge_pdfs( return out_filepath -def pdf_to_cmyk(input_file: os.Pathlike | str, output_file: os.Pathlike | str) -> int: +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. diff --git a/docstamp/qrcode.py b/docstamp/qrcode.py index daa5b83..43eadb1 100644 --- a/docstamp/qrcode.py +++ b/docstamp/qrcode.py @@ -53,7 +53,7 @@ def save_into_qrcode( out_filepath: os.PathLike | str, color: str = "", box_size: float = 10, -): +) -> None: """Save `text` in a qrcode svg image file. Parameters @@ -71,25 +71,12 @@ def save_into_qrcode( Size of the QR code boxes. """ img = _create_qrcode_image(text=text, box_size=box_size) - _ = _save_qrcode(qrcode=img, out_filepath=out_filepath) - if color: - replace_file_content(out_filepath, "fill:#000000", f"fill:#{color}") - - -def _save_qrcode(qrcode: qrcode.image.svg.SvgPathImage, out_filepath: str): - """Save a `qrcode` object into `out_filepath`. - Parameters - ---------- - qrcode: qrcode object - - out_filepath: str - Path to the output file. - """ try: - qrcode.save(out_filepath) + img.save(out_filepath) except Exception as 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_utils.py b/docstamp/svg_utils.py index 230a2c0..85e1567 100644 --- a/docstamp/svg_utils.py +++ b/docstamp/svg_utils.py @@ -3,12 +3,13 @@ from __future__ import annotations import os +import shutil from pathlib import Path import svgutils import svgutils.transform as sg -from docstamp.commands import call_command, check_command, which +from docstamp.commands import call_command, check_command def replace_chars_for_svg_code(svg_content: str) -> str: @@ -140,11 +141,11 @@ def rsvg_export( Command call return value """ _input_file = Path(input_file) - if not input_file.exists(): + if not _input_file.exists(): raise FileNotFoundError(f"File {input_file} not found.") if rsvg_binpath is None: - rsvg_binpath = which(cmd_name="rsvg-convert") + rsvg_binpath = shutil.which(cmd_name="rsvg-convert") check_command(cmd_name=rsvg_binpath) args_strings = [ diff --git a/docstamp/template.py b/docstamp/template.py index 71d7aa4..7f07fb7 100644 --- a/docstamp/template.py +++ b/docstamp/template.py @@ -104,7 +104,7 @@ def export( self, file_path: os.PathLike | str, doc_contents: dict[str, Any], - encoding: str = "utf-8", + **kwargs: Any, ): """Export the rendered document to a file. @@ -119,6 +119,7 @@ def export( """ rendered_content = self.render(doc_contents=doc_contents) _file_path = Path(file_path) + encoding = kwargs.get("encoding", "utf-8") try: _file_path.write_text( rendered_content, @@ -188,7 +189,10 @@ def render(self, doc_contents: dict[str, Any]) -> str: ) from error def export( - self, file_path: os.PathLike | str, doc_contents: dict[str, Any], **kwargs + self, + file_path: os.PathLike | str, + doc_contents: dict[str, Any], + **kwargs, ): """Export the content of the .svg file in the chosen rendered format. diff --git a/docstamp/unicode_csv.py b/docstamp/unicode_csv.py deleted file mode 100644 index dd18d60..0000000 --- a/docstamp/unicode_csv.py +++ /dev/null @@ -1,45 +0,0 @@ -from __future__ import annotations - -import codecs -import csv -from io import StringIO -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from csv import Dialect - - -class UnicodeWriter: - """ - A CSV writer which will write rows to CSV file "f", - which is encoded in the given encoding. - """ - - def __init__( - self, - file: StringIO, - dialect: Dialect = csv.excel, - encoding: str = "utf-8", - **kwargs, - ): - # Redirect output to a queue - self.queue = StringIO() - self.writer = csv.writer(self.queue, dialect=dialect, **kwargs) - self.stream = file - self.encoder = codecs.getincrementalencoder(encoding)() - - def writerow(self, row: list[str]): - 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: list[list[str]]): - for row in rows: - self.writerow(row) diff --git a/docstamp/version.py b/docstamp/version.py index cd1ee63..da793c1 100644 --- a/docstamp/version.py +++ b/docstamp/version.py @@ -1 +1 @@ -__version__ = "0.4.4" +__version__ = "0.4.4.post9.dev0+f77fcf8" diff --git a/pyproject.toml b/pyproject.toml index 6173eb1..d429ffb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ dependencies = [ "qrcode>=6.1", "svgutils==0.3.1", "click>=7.0", + "orjson>=3.10.18", ] authors = [ {name = "Alexandre Manhaes Savio", email = "alexsavio@github.com"}, diff --git a/uv.lock b/uv.lock index eb277aa..b81702c 100644 --- a/uv.lock +++ b/uv.lock @@ -144,6 +144,7 @@ source = { editable = "." } dependencies = [ { name = "click" }, { name = "jinja2" }, + { name = "orjson" }, { name = "pillow" }, { name = "pypdf2" }, { name = "qrcode" }, @@ -162,6 +163,7 @@ dev = [ 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" }, @@ -501,6 +503,44 @@ 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" From 94b228c24253ec1365ff0f0c4dd8f0b1c8a53758 Mon Sep 17 00:00:00 2001 From: alexsavio Date: Sat, 21 Jun 2025 18:31:48 +0200 Subject: [PATCH 3/4] fix --- docstamp/cli/__init__.py | 1 - docstamp/cli/{cli.py => main.py} | 17 +++--- docstamp/cli/utils.py | 6 ++- docstamp/commands.py | 17 +++--- docstamp/config.py | 89 +++++++++++--------------------- docstamp/file_utils.py | 10 ++-- docstamp/inkscape.py | 25 +++++---- docstamp/pdf_utils.py | 2 +- docstamp/pdflatex.py | 41 ++++++++------- docstamp/svg_fonts.py | 28 ++++++---- docstamp/svg_utils.py | 31 +++++++---- docstamp/template.py | 52 ++++++++++--------- pyproject.toml | 2 +- 13 files changed, 158 insertions(+), 163 deletions(-) rename docstamp/cli/{cli.py => main.py} (95%) 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/main.py similarity index 95% rename from docstamp/cli/cli.py rename to docstamp/cli/main.py index dc9e923..ce45922 100644 --- a/docstamp/cli/cli.py +++ b/docstamp/cli/main.py @@ -3,9 +3,8 @@ import logging import math -import os -from pathlib import Path import sys +from pathlib import Path import click @@ -14,9 +13,7 @@ DirPath, ExistingFilePath, get_items_from_csv, - verbose_switch, ) -from docstamp.config import LOGGING_LVL from docstamp.file_utils import get_extension from docstamp.template import TextDocument @@ -122,7 +119,7 @@ def create( # noqa: C901, PLR0912, PLR0913, PLR0915 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) + logging.basicConfig(level="INFO") log = logging.getLogger(__name__) # setup verbose mode @@ -188,7 +185,7 @@ def create( # noqa: C901, PLR0912, PLR0913, PLR0915 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) + log.exception("Could not get field %s value from %s.", field_name, item) sys.exit(-1) else: file_name = "_".join(field_values) @@ -196,8 +193,8 @@ def create( # noqa: C901, PLR0912, PLR0913, PLR0915 log.debug("Filling template %s with values of item %s.", file_name, idx) try: template_doc.render(item) - except: - log.exception("Error filling document for %sth item", idx) + except Exception as error: + log.exception("Error filling document for %sth item: %s.", idx, error) continue # set output file path @@ -216,8 +213,8 @@ def create( # noqa: C901, PLR0912, PLR0913, PLR0915 dpi=dpi, support_unicode=unicode_support, ) - except: - log.exception("Error creating %s for %s.", file_path, item) + 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 9157f1e..980eb90 100644 --- a/docstamp/cli/utils.py +++ b/docstamp/cli/utils.py @@ -13,6 +13,7 @@ import orjson if TYPE_CHECKING: + from collections.abc import Sequence from typing import Any # different context options @@ -31,7 +32,8 @@ class RegularExpression(click.ParamType): name = "regex" - def convert(self, value: str, param: str, ctx: click.Context | None) -> re.Pattern: + 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: return re.compile(value, re.IGNORECASE) except ValueError: @@ -40,7 +42,7 @@ def convert(self, value: str, param: str, ctx: click.Context | None) -> re.Patte def get_items_from_csv( csv_filepath: os.PathLike | str, -) -> tuple[dict[int, Any], list[str] | None]: +) -> 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. diff --git a/docstamp/commands.py b/docstamp/commands.py index dcaebbf..947c536 100644 --- a/docstamp/commands.py +++ b/docstamp/commands.py @@ -1,6 +1,7 @@ from __future__ import annotations import logging +import os import shutil import subprocess from pathlib import Path @@ -14,15 +15,7 @@ def simple_call(cmd_args: list[str]) -> int: return subprocess.call(" ".join(cmd_args), shell=True) # noqa: S602 -def check_command(cmd_name): - """Raise a FileNotFoundError if the command is not found. - :param cmd_name: - """ - if shutil.which(cmd_name) is None: - raise FileNotFoundError(f"Could not find command named {cmd_name}.") - - -def call_command(cmd_name: str, args_strings: list[str]) -> int: +def call_command(cmd_name: str | os.PathLike, args_strings: list[str]) -> int: """Call CLI command with arguments and returns its return value. Parameters @@ -39,13 +32,17 @@ def call_command(cmd_name: str, args_strings: list[str]) -> int: Command return value. """ 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] + 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 diff --git a/docstamp/config.py b/docstamp/config.py index a99c5b3..80d819a 100644 --- a/docstamp/config.py +++ b/docstamp/config.py @@ -35,6 +35,7 @@ def find_file_match(folder_path: Path, regex: str = ".*") -> list[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 [ Path("/opt/bin"), @@ -48,23 +49,32 @@ def get_other_program_folders() -> list[Path]: return [Path("/Applications"), Path(os.environ["HOME"]) / "Applications"] elif _platform == "win32": # don't know if this works - return Path(r"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: str) -> Path | None: - app_name_regex = "^" + app_name + "$" + """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: @@ -87,52 +97,19 @@ def is_executable(filepath: str | Path) -> bool: filepath = Path(filepath) if not filepath.exists(): return False - if _platform == "win32": + 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: - return filepath.is_file() and os.access(filepath, os.X_OK) - - -def ask_for_path_of(app_name: str) -> Path | None: - """Ask the user for the path of the application binary. - This function will repeatedly prompt the user until a valid executable - file is provided. - Parameters - ---------- - app_name: str - The name of the application to find. - Returns - ------- - str - The path to the binary file. - Raises - ------ - ValueError - If the provided path does not exist or is not executable. - """ - bin_path = None - while bin_path is not None: - bin_path = input( - f"Insert path of {app_name} executable file [Press Ctrl+C to exit]: " + raise NotImplementedError( + f"Platform {_platform} is not supported for checking executable files." ) - if not Path(bin_path).exists(): - print(f"Could not find file {bin_path}. Try it again.") - bin_path = None - continue - if not is_executable(bin_path): - print(f"No execution permissions on file {bin_path}. Try again.") - bin_path = None - continue - - return Path(bin_path) if bin_path else None - - -def proactive_search_of(app_name: str) -> Path | None: - """Proactively search for the binary of the given application. - This function checks the system PATH, common program folders, and prompts - the user for the path if the binary is not found. +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 @@ -155,7 +132,10 @@ def proactive_search_of(app_name: str) -> Path | None: 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() -> Path | None: @@ -163,17 +143,10 @@ def get_inkscape_binpath() -> Path | None: 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 + return get_executable_path(bin_name) def get_lyx_binpath() -> Path | None: """Return the LyX binary path.""" - if "LYX_BINPATH" not in globals(): - global LYX_BINPATH - LYX_BINPATH = proactive_search_of("lyx") - return LYX_BINPATH + bin_name = "lyx" + return get_executable_path(bin_name) diff --git a/docstamp/file_utils.py b/docstamp/file_utils.py index 149062b..9dba59c 100644 --- a/docstamp/file_utils.py +++ b/docstamp/file_utils.py @@ -44,14 +44,14 @@ def remove_ext(filepath: os.PathLike | str) -> str: """ extension = get_extension(filepath) if not extension: - return filepath - return filepath.removesuffix(extension) + return str(filepath) + return str(filepath).removesuffix(extension) def get_tempfile( suffix: str = ".txt", dirpath: os.PathLike | str | None = None, -) -> tempfile.NamedTemporaryFile: +) -> 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. @@ -71,7 +71,7 @@ def get_tempfile( 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: os.PathLike | str, extension: str): @@ -114,5 +114,5 @@ def replace_file_content(filepath: os.PathLike | str, old: str, new: str, max: i """ _filepath = Path(filepath) content = _filepath.read_text() - content = content.replace(old=old, new=new, count=max) + content = content.replace(old, new, max) _filepath.write_text(content) diff --git a/docstamp/inkscape.py b/docstamp/inkscape.py index d34442b..8271748 100644 --- a/docstamp/inkscape.py +++ b/docstamp/inkscape.py @@ -27,12 +27,10 @@ def call_inkscape( 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 Path.exists(inkscape_binpath): + if inkscape_binpath is None or not Path(inkscape_binpath).exists(): raise FileNotFoundError( "Inkscape binary has not been found. Please check configuration." ) @@ -75,20 +73,21 @@ def inkscape_export( Command call return value """ - if not Path.exists(input_file): + if not Path(input_file).exists(): raise FileNotFoundError(f"File {input_file} not found.") if "=" not in export_flag: export_flag += " " - arg_strings = [] - arg_strings += ["--without-gui"] - arg_strings += ["--export-text-to-path"] - arg_strings += ["--export-pdf-version=1.5"] - arg_strings += [f'{export_flag}"{output_file}"'] - arg_strings += [f"--export-dpi={dpi}"] - arg_strings += [f'"{input_file}"'] - return call_inkscape(arg_strings=arg_strings, inkscape_binpath=inkscape_binpath) + 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( @@ -125,7 +124,7 @@ def svg2png( ) -> int: """Transform SVG file to PNG file""" return inkscape_export( - intput_file=svg_file_path, + input_file=svg_file_path, output_file=png_file_path, export_flag="-e", dpi=dpi, diff --git a/docstamp/pdf_utils.py b/docstamp/pdf_utils.py index 873b87a..750cd0d 100644 --- a/docstamp/pdf_utils.py +++ b/docstamp/pdf_utils.py @@ -35,7 +35,7 @@ def merge_pdfs( merger.append(PdfFileReader(pdf_filepath.open("rb"))) merger.write(str(out_filepath)) - return out_filepath + return Path(out_filepath) def pdf_to_cmyk(input_file: os.PathLike | str, output_file: os.PathLike | str) -> int: diff --git a/docstamp/pdflatex.py b/docstamp/pdflatex.py index b757462..d9efc34 100644 --- a/docstamp/pdflatex.py +++ b/docstamp/pdflatex.py @@ -8,7 +8,7 @@ from pathlib import Path from typing import Literal -from docstamp.commands import check_command, simple_call +from docstamp.commands import simple_call from docstamp.file_utils import cleanup, remove_ext log = logging.getLogger(__name__) @@ -66,22 +66,21 @@ def tex2pdf( tex_file_path = Path(tex_file) output_file_path = Path(output_file) if output_file else None _check_latex_file_inputs( - tex_file=tex_file_path, - output_file=output_file_path, + tex_file_path=tex_file_path, + output_file_path=output_file_path, output_format=output_format, ) cmd_name = "pdflatex" - check_command(cmd_name=cmd_name) + if shutil.which(cmd=cmd_name) is None: + raise FileNotFoundError(f"Could not find command named {cmd_name}.") args_strings = [cmd_name] - result_dir = "" - if output_file is not None: + 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 - else: - result_dir = tex_file_path.parent args_strings += [f'-output-format="{output_format}"'] args_strings += [f'"{tex_file}"'] @@ -91,11 +90,12 @@ def tex2pdf( tex_file_name = f"{remove_ext(tex_file_path.name)}.{output_format}" result_file = result_dir / tex_file_name - if result_file.exists(): - shutil.move(result_file, output_file_path) - else: + if not result_file.exists(): raise FileNotFoundError("Could not find PDFLatex result file.") + if output_file_path is not None: + shutil.move(result_file, output_file_path) + _cleanup_aux_log_files(workdir=result_dir) return exit_code @@ -127,21 +127,21 @@ def xetex2pdf( tex_file_path = Path(tex_file) output_file_path = Path(output_file) if output_file else None _check_latex_file_inputs( - tex_file=tex_file_path, - output_file=output_file_path, + tex_file_path=tex_file_path, + output_file_path=output_file_path, output_format=output_format, ) cmd_name = "xelatex" - check_command(cmd_name=cmd_name) + 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: + 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 - else: - result_dir = tex_file_path.parent if output_format == "dvi": args_strings += ["-no-pdf"] @@ -153,10 +153,11 @@ def xetex2pdf( tex_file_name = f"{remove_ext(tex_file_path.name)}.{output_format}" result_file = result_dir / tex_file_name - if result_file.exists(): - shutil.move(result_file, output_file_path) - else: + if not result_file.exists(): raise FileNotFoundError("Could not find XeLatex result file.") + if output_file_path is not None: + shutil.move(result_file, output_file_path) + _cleanup_aux_log_files(workdir=result_dir) return exit_code diff --git a/docstamp/svg_fonts.py b/docstamp/svg_fonts.py index e015ef6..62866a9 100644 --- a/docstamp/svg_fonts.py +++ b/docstamp/svg_fonts.py @@ -15,13 +15,16 @@ from typing import Literal -FONT_TYPES = {"ttf": "truetype", "otf": "opentype"} +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""" _bin_filepath = Path(bin_filepath) - return base64.b64encode(_bin_filepath.open().read()) + return base64.b64encode(_bin_filepath.open(mode="rb").read()) def remove_ext(filepath: os.PathLike | str) -> str: @@ -83,9 +86,15 @@ def base64(self) -> bytes: 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(filepath=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) -> str: @@ -95,11 +104,12 @@ def ext(self) -> str: @property def css_text(self) -> str: """Return the CSS text for embedding the font.""" - 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" + 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 diff --git a/docstamp/svg_utils.py b/docstamp/svg_utils.py index 85e1567..3c4f892 100644 --- a/docstamp/svg_utils.py +++ b/docstamp/svg_utils.py @@ -9,7 +9,7 @@ import svgutils import svgutils.transform as sg -from docstamp.commands import call_command, check_command +from docstamp.commands import call_command def replace_chars_for_svg_code(svg_content: str) -> str: @@ -114,26 +114,35 @@ def merge_svg_files( def rsvg_export( input_file: os.PathLike | str, - output_file: str | Path, + output_file: os.PathLike | str, dpi: int = 90, - rsvg_binpath: str | None = None, + rsvg_binpath: os.PathLike | str | None = None, ): """Calls the `rsvg-convert` command, to convert a svg to a PDF (with unicode). Parameters ---------- - 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: str - Path to `rsvg-convert` command + 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 ------- @@ -145,15 +154,17 @@ def rsvg_export( raise FileNotFoundError(f"File {input_file} not found.") if rsvg_binpath is None: - rsvg_binpath = shutil.which(cmd_name="rsvg-convert") - check_command(cmd_name=rsvg_binpath) + 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}", - input_file, + str(input_file), ] return call_command(cmd_name=rsvg_binpath, args_strings=args_strings) diff --git a/docstamp/template.py b/docstamp/template.py index 7f07fb7..7a28274 100644 --- a/docstamp/template.py +++ b/docstamp/template.py @@ -20,8 +20,8 @@ def get_doctype_by_extension( - extension: Literal["txt", "svg", "tex"], -) -> type[TextDocument]: + extension: Literal["txt", "svg", "tex"] | str, +) -> type[TextDocument] | None: if "txt" in extension: doc_type = TextDocument elif "svg" in extension: @@ -29,15 +29,13 @@ def get_doctype_by_extension( elif "tex" in extension: doc_type = LateXDocument else: - raise ValueError( - f"Could not determine the document type for `extension` {extension}." - ) + doc_type = None return doc_type def get_doctype_by_command( command: Literal["inkscape", "pdflatex", "xelatex"] | None, -) -> type[TextDocument]: +) -> type[TextDocument] | None: if not command: doc_type = TextDocument elif command == "inkscape": @@ -47,9 +45,7 @@ def get_doctype_by_command( elif command == "xelatex": doc_type = XeLateXDocument else: - raise ValueError( - f"Could not determine the document type for `command` {command}." - ) + doc_type = None # type: ignore[unreachable] return doc_type @@ -78,7 +74,7 @@ def __init__( loader=FileSystemLoader(self._template_file.parent), autoescape=False, # noqa: S701 ) - self.template = self._template_env.get_template(template_file_path.name) + self.template = self._template_env.get_template(self._template_file.name) def render(self, doc_contents: dict[str, Any] | None = None) -> str: """Render the content of the document with the information in doc_contents. @@ -129,7 +125,11 @@ def export( raise ExportError(f"Error exporting document to {file_path}.") from error @classmethod - def from_template_file(cls, template_file_path: Path, command=None): + 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`. @@ -149,20 +149,23 @@ class given by the `command` or the extension of `template_file_path`. # get template file extension 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 TextDocument.""" - def render(self, doc_contents: dict[str, Any]) -> str: + 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 @@ -178,6 +181,9 @@ def render(self, doc_contents: dict[str, Any]) -> str: 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) @@ -282,19 +288,19 @@ def export( rendered_content = self.render(doc_contents=doc_contents) _file_path = Path(file_path) try: - file_path.write_text( + _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}." + f"Error exporting TeX document to {_file_path}." ) from error try: - self._export(temp.name, file_path, output_format="pdf") + 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 + raise ExportError(f"Error exporting file {_file_path} to PDF.") from error class PDFLateXDocument(LateXDocument): diff --git a/pyproject.toml b/pyproject.toml index d429ffb..efd370c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,7 +67,7 @@ __version__ = "{version}" ''' [project.scripts] -docstamp = "docstamp.cli.cli:cli" +docstamp = "docstamp.cli.main:cli" [tool.uv] default-groups = "all" From 47ed6f916394b79f6a10d11f8ecc8fc1e991b1b9 Mon Sep 17 00:00:00 2001 From: alexsavio Date: Sat, 21 Jun 2025 18:36:41 +0200 Subject: [PATCH 4/4] fix --- docstamp/cli/main.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docstamp/cli/main.py b/docstamp/cli/main.py index ce45922..991bcda 100644 --- a/docstamp/cli/main.py +++ b/docstamp/cli/main.py @@ -112,8 +112,7 @@ def create( # noqa: C901, PLR0912, PLR0913, PLR0915 verbose, unicode_support, ): - """Use docstamp to create documents from the content of a CSV file or - a Google Spreadsheet. + """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 @@ -178,7 +177,7 @@ def create( # noqa: C901, PLR0912, PLR0913, PLR0915 item = items[idx] if not fields: - file_name = str(idx).zfill(n_zeros) + file_suffix = str(idx).zfill(n_zeros) else: field_values = [] try: @@ -188,9 +187,9 @@ def create( # noqa: C901, PLR0912, PLR0913, PLR0915 log.exception("Could not get field %s value from %s.", field_name, item) sys.exit(-1) else: - file_name = "_".join(field_values) + file_suffix = "_".join(field_values) - log.debug("Filling template %s with values of item %s.", file_name, idx) + log.debug("Filling template %s with values of item %s.", file_suffix, idx) try: template_doc.render(item) except Exception as error: @@ -204,7 +203,7 @@ def create( # noqa: C901, PLR0912, PLR0913, PLR0915 else: basename = prefix - file_path = output_directory / f"{basename + "_" + file_name}.{otype}" + file_path = output_directory / f"{basename}_{file_suffix}.{otype}" log.debug("Rendering file %s.", file_path) try: template_doc.export(