├── .dockerignore
├── .github
└── workflows
│ ├── lint-and-tests.yml
│ └── release.yml
├── .gitignore
├── .pre-commit-config.yaml
├── CHANGELOG.md
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── pyproject.toml
├── ruff.toml
├── secure_qrcode
├── __init__.py
├── api.py
├── config.py
├── crypto.py
├── exceptions.py
├── models.py
└── qrcode.py
├── static
├── android-chrome-192x192.png
├── android-chrome-512x512.png
├── apple-touch-icon.png
├── browserconfig.xml
├── favicon-16x16.png
├── favicon-32x32.png
├── favicon.ico
├── github.svg
├── mstile-150x150.png
├── site.webmanifest
└── styles.css
├── templates
└── index.html
├── tests
├── __init__.py
├── conftest.py
├── test_api.py
├── test_crypto.py
└── test_qrcode.py
├── uv.lock
└── version.txt
/.dockerignore:
--------------------------------------------------------------------------------
1 | .git/
2 | .github/
3 | .pytest_cache/
4 | .coverage
5 | .ruff_cache/
6 |
--------------------------------------------------------------------------------
/.github/workflows/lint-and-tests.yml:
--------------------------------------------------------------------------------
1 | name: Execute lint and tests
2 |
3 | on:
4 | workflow_call:
5 | push:
6 | branches:
7 | - "**"
8 | - "!main"
9 |
10 | jobs:
11 | tests:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Install system dependencies
15 | run: sudo apt update && sudo apt install --no-install-recommends -y make git
16 | - uses: actions/checkout@v4
17 | - uses: actions/cache@v4
18 | with:
19 | path: ~/.cache
20 | key: self-runner-${{ runner.os }}-python-3.13-${{ hashFiles('uv.lock') }}-precommit-${{ hashFiles('.pre-commit-config.yaml') }}
21 | - name: Set up Python 3.13
22 | uses: actions/setup-python@v5
23 | with:
24 | python-version: "3.13"
25 | - name: Install dependencies
26 | run: |
27 | python -m pip install --upgrade pip
28 | pip install uv
29 | uv sync --frozen
30 | - name: pre-commit lint
31 | run: make lint
32 | - name: pytest
33 | run: make test
34 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Execute lint/tests/release
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | permissions:
9 | contents: write
10 | pull-requests: write
11 |
12 | jobs:
13 | build:
14 | uses: ./.github/workflows/lint-and-tests.yml
15 | release-please:
16 | needs: build
17 | runs-on: ubuntu-latest
18 | steps:
19 | - uses: GoogleCloudPlatform/release-please-action@v3
20 | with:
21 | token: ${{ secrets.GITHUB_TOKEN }}
22 | release-type: simple
23 | package-name: secure-qrcode
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # poetry
98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102 | #poetry.lock
103 |
104 | # pdm
105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106 | #pdm.lock
107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108 | # in version control.
109 | # https://pdm.fming.dev/#use-with-ide
110 | .pdm.toml
111 |
112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113 | __pypackages__/
114 |
115 | # Celery stuff
116 | celerybeat-schedule
117 | celerybeat.pid
118 |
119 | # SageMath parsed files
120 | *.sage.py
121 |
122 | # Environments
123 | .env
124 | .venv
125 | env/
126 | venv/
127 | ENV/
128 | env.bak/
129 | venv.bak/
130 |
131 | # Spyder project settings
132 | .spyderproject
133 | .spyproject
134 |
135 | # Rope project settings
136 | .ropeproject
137 |
138 | # mkdocs documentation
139 | /site
140 |
141 | # mypy
142 | .mypy_cache/
143 | .dmypy.json
144 | dmypy.json
145 |
146 | # Pyre type checker
147 | .pyre/
148 |
149 | # pytype static type analyzer
150 | .pytype/
151 |
152 | # Cython debug symbols
153 | cython_debug/
154 |
155 | # PyCharm
156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
158 | # and can be added to the global gitignore or merged into this file. For a more nuclear
159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
160 | #.idea/
161 |
162 | # Ruff cache
163 | .ruff_cache/
164 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/pre-commit/pre-commit-hooks
3 | rev: v5.0.0
4 | hooks:
5 | - id: check-ast
6 | - id: fix-byte-order-marker
7 | - id: check-docstring-first
8 | - id: check-json
9 | - id: check-merge-conflict
10 | - id: check-symlinks
11 | - id: check-toml
12 | - id: check-vcs-permalinks
13 | - id: check-xml
14 | - id: check-yaml
15 | - id: debug-statements
16 | - id: destroyed-symlinks
17 | - id: end-of-file-fixer
18 | - id: trailing-whitespace
19 |
20 | - repo: https://github.com/astral-sh/ruff-pre-commit
21 | rev: v0.11.4
22 | hooks:
23 | - id: ruff
24 | - id: ruff-format
25 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## [2.0.0](https://github.com/allisson/secure-qrcode/compare/v1.6.0...v2.0.0) (2025-04-05)
4 |
5 |
6 | ### ⚠ BREAKING CHANGES
7 |
8 | * this new version "2.x" is incompatible with version "1.x"
9 |
10 | ### Features
11 |
12 | * use PBKDF2 Key derivation function ([#34](https://github.com/allisson/secure-qrcode/issues/34)) ([9746164](https://github.com/allisson/secure-qrcode/commit/97461646e79a9ba2f6f1e140d7d2ebe1139ca378))
13 |
14 | ## [1.6.0](https://github.com/allisson/secure-qrcode/compare/v1.5.0...v1.6.0) (2025-04-05)
15 |
16 |
17 | ### Features
18 |
19 | * update python version to 3.13 ([#32](https://github.com/allisson/secure-qrcode/issues/32)) ([6101c73](https://github.com/allisson/secure-qrcode/commit/6101c73665a9edc4846e6228d164a2490de34ddd))
20 |
21 | ## [1.5.0](https://github.com/allisson/secure-qrcode/compare/v1.4.3...v1.5.0) (2024-09-17)
22 |
23 |
24 | ### Features
25 |
26 | * switch to uv package manager ([#30](https://github.com/allisson/secure-qrcode/issues/30)) ([76d90a0](https://github.com/allisson/secure-qrcode/commit/76d90a0ca0f89252bf2253735f1a9654d6936651))
27 |
28 | ## [1.4.3](https://github.com/allisson/secure-qrcode/compare/v1.4.2...v1.4.3) (2024-07-12)
29 |
30 |
31 | ### Bug Fixes
32 |
33 | * update certifi version ([#28](https://github.com/allisson/secure-qrcode/issues/28)) ([48bba64](https://github.com/allisson/secure-qrcode/commit/48bba6427ca3ff0dc777365aa4c63c59d7f8af7e))
34 |
35 | ## [1.4.2](https://github.com/allisson/secure-qrcode/compare/v1.4.1...v1.4.2) (2024-06-24)
36 |
37 |
38 | ### Bug Fixes
39 |
40 | * update requirements version ([#26](https://github.com/allisson/secure-qrcode/issues/26)) ([10894a8](https://github.com/allisson/secure-qrcode/commit/10894a841322a20a01fdd5a73b2fadef0c2d34fc))
41 |
42 | ## [1.4.1](https://github.com/allisson/secure-qrcode/compare/v1.4.0...v1.4.1) (2024-04-04)
43 |
44 |
45 | ### Bug Fixes
46 |
47 | * update fastapi version ([#24](https://github.com/allisson/secure-qrcode/issues/24)) ([005dcc9](https://github.com/allisson/secure-qrcode/commit/005dcc91470cfa48d834765ea0bac7fc45885c16))
48 |
49 | ## [1.4.0](https://github.com/allisson/secure-qrcode/compare/v1.3.1...v1.4.0) (2024-04-01)
50 |
51 |
52 | ### Features
53 |
54 | * switch the lint to ruff ([#22](https://github.com/allisson/secure-qrcode/issues/22)) ([1997a29](https://github.com/allisson/secure-qrcode/commit/1997a29b58e2e08c9b7e6d7e1ac8613730376540))
55 |
56 | ## [1.3.1](https://github.com/allisson/secure-qrcode/compare/v1.3.0...v1.3.1) (2024-01-05)
57 |
58 |
59 | ### Bug Fixes
60 |
61 | * update the pycryptodome version ([#20](https://github.com/allisson/secure-qrcode/issues/20)) ([2a7baee](https://github.com/allisson/secure-qrcode/commit/2a7baee0d6f38cdb3cb46fbacc62db79327c76af))
62 |
63 | ## [1.3.0](https://github.com/allisson/secure-qrcode/compare/v1.2.1...v1.3.0) (2023-12-11)
64 |
65 |
66 | ### Features
67 |
68 | * improve Dockerfile ([#18](https://github.com/allisson/secure-qrcode/issues/18)) ([866411e](https://github.com/allisson/secure-qrcode/commit/866411e3fa0fba70e80e37b9dda1f8f1341a154b))
69 |
70 | ## [1.2.1](https://github.com/allisson/secure-qrcode/compare/v1.2.0...v1.2.1) (2023-12-10)
71 |
72 |
73 | ### Bug Fixes
74 |
75 | * Change templates.env.globals["url_for"] ([#14](https://github.com/allisson/secure-qrcode/issues/14)) ([da8ac8a](https://github.com/allisson/secure-qrcode/commit/da8ac8a98eb5ed55b2ca13e977862566a5852dee))
76 |
77 | ## [1.2.0](https://github.com/allisson/secure-qrcode/compare/v1.1.0...v1.2.0) (2023-12-10)
78 |
79 |
80 | ### Features
81 |
82 | * first frontend version ([#12](https://github.com/allisson/secure-qrcode/issues/12)) ([760f2d4](https://github.com/allisson/secure-qrcode/commit/760f2d48e19f9a2dd43a0b1839853bc4daf84819))
83 |
84 | ## [1.1.0](https://github.com/allisson/secure-qrcode/compare/v1.0.1...v1.1.0) (2023-11-29)
85 |
86 |
87 | ### Features
88 |
89 | * add option to change the left padding char ([#9](https://github.com/allisson/secure-qrcode/issues/9)) ([3559857](https://github.com/allisson/secure-qrcode/commit/355985718bd9ea5f7833088e63a49cb7048e4ed8))
90 |
91 | ## [1.0.1](https://github.com/allisson/secure-qrcode/compare/v1.0.0...v1.0.1) (2023-11-22)
92 |
93 |
94 | ### Bug Fixes
95 |
96 | * fix the docker run command in the readme ([#6](https://github.com/allisson/secure-qrcode/issues/6)) ([cabd90d](https://github.com/allisson/secure-qrcode/commit/cabd90d55b44b8bd24fdaeea9b060bf5962986dd))
97 |
98 | ## 1.0.0 (2023-11-22)
99 |
100 |
101 | ### Features
102 |
103 | * first version ([#1](https://github.com/allisson/secure-qrcode/issues/1)) ([4dacef2](https://github.com/allisson/secure-qrcode/commit/4dacef21043139039c092a05e4541db5364535da))
104 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | ##### Base Stage #####
2 | FROM python:3.13-slim-bookworm AS base
3 |
4 | # Set default path
5 | ENV PATH="/app/.venv/bin:${PATH}"
6 |
7 | ##### Builder Stage #####
8 | FROM base AS builder
9 |
10 | # Set default workdir
11 | WORKDIR /app
12 |
13 | # Create virtualenv and install Python packages
14 | RUN pip install --no-cache-dir pip -U && \
15 | pip install --no-cache-dir uv
16 | COPY ./uv.lock uv.lock
17 | COPY ./pyproject.toml pyproject.toml
18 | RUN uv sync --no-dev --frozen
19 |
20 | # Copy app files to workdir
21 | COPY secure_qrcode ./secure_qrcode
22 | COPY templates ./templates
23 | COPY static ./static
24 |
25 | ##### Final Stage #####
26 | FROM base
27 |
28 | # Copy content from builder stage
29 | COPY --from=builder /app /app
30 |
31 | # Add qrcode user and create directories
32 | RUN useradd -m qrcode
33 |
34 | # Set permissions
35 | RUN chown -R qrcode:qrcode /app
36 |
37 | # Set workdir and user
38 | WORKDIR /app
39 | USER qrcode
40 |
41 | # Expose port
42 | EXPOSE 8000
43 |
44 | # Set entrypoint and cmd
45 | ENTRYPOINT ["uvicorn", "--host", "0.0.0.0", "--port", "8000", "secure_qrcode.api:app"]
46 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Allisson Azevedo
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: test
2 | test:
3 | uv run pytest -v
4 |
5 | .PHONY: lint
6 | lint:
7 | uv run pre-commit run --all-files
8 |
9 | .PHONY: run-api
10 | run-api:
11 | uv run uvicorn secure_qrcode.api:app --reload
12 |
13 | .PHONY: docker-build
14 | docker-build:
15 | docker build --rm -t allisson/secure-qrcode .
16 |
17 | .PHONY: docker-run
18 | docker-run:
19 | docker run --rm -p 8000:8000 allisson/secure-qrcode
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # secure-qrcode
2 | [](https://github.com/allisson/secure-qrcode/actions)
3 | [](https://quay.io/repository/allisson/secure-qrcode)
4 |
5 | Encrypt your data using the modern ChaCha20-Poly1305 cipher and export it into a secure QR code.
6 |
7 | ## about the versions
8 |
9 | The current version "2.x" is incompatible with version "1.x".
10 |
11 | To encrypt and decrypt QR codes using version "1.x" use this documentation: https://github.com/allisson/secure-qrcode/tree/v1.6.0
12 |
13 | ## how it works
14 |
15 | The system receives your key and uses a key derivation function with PBKDF2 to obtain a 32-byte derived key to be applied to the ChaCha20-Poly1305 algorithm.
16 |
17 | ## access via browser
18 |
19 | Open the url https://secure-qrcode.onrender.com on your browser (This is a free instance type and will stop upon inactivity, so be patient).
20 |
21 | If you want to run this on your local machine, see the next section.
22 |
23 | ## run the api
24 |
25 | The server can be started using a docker image:
26 |
27 | ```bash
28 | docker run --rm -p 8000:8000 quay.io/allisson/secure-qrcode
29 | ```
30 |
31 | Now the API server will be running on port 8000 and you can open the url http://localhost:8000 on your browser.
32 |
33 | ## api documentation
34 |
35 | You can access the API documentation using these two endpoints:
36 | - http://localhost:8000/docs
37 | - http://localhost:8000/redoc
38 |
39 | ## generate a secure QR code
40 |
41 | Call the API passing at least the plaintext and key fields:
42 |
43 | ```bash
44 | curl --location 'http://localhost:8000/v1/encode' \
45 | --header 'Content-Type: application/json' \
46 | --data '{
47 | "plaintext": "my super secret text",
48 | "key": "my super secret key"
49 | }' | jq -r '.content' | base64 --decode > qrcode.png
50 | ```
51 |
52 | Now you can open the qrcode.png file and do whatever you want.
53 |
54 | ## decrypt the QR code
55 |
56 | Use any program that read a QR code, the content will be something like this:
57 |
58 | ```json
59 | {
60 | "salt": "LC1bxUNUpMnt/mae1KDNiA==",
61 | "iterations": 1200000,
62 | "associated_data": "0WdPVTKSb/a6KjB3NgjFww==",
63 | "nonce": "FgmmR8D1Su13HgUO",
64 | "ciphertext": "4FIQ8LAlztzaToyElulDcPAReKGnOd2TFYiH1P9ZatIOuHN+"
65 | }
66 | ```
67 |
68 | Now call the API passing the encrypted_data and the key:
69 |
70 | ```bash
71 | curl --location 'http://localhost:8000/v1/decode' \
72 | --header 'Content-Type: application/json' \
73 | --data '{
74 | "encrypted_data": {
75 | "salt": "LC1bxUNUpMnt/mae1KDNiA==",
76 | "iterations": 1200000,
77 | "associated_data": "0WdPVTKSb/a6KjB3NgjFww==",
78 | "nonce": "FgmmR8D1Su13HgUO",
79 | "ciphertext": "4FIQ8LAlztzaToyElulDcPAReKGnOd2TFYiH1P9ZatIOuHN+"
80 | },
81 | "key": "my super secret key"
82 | }' | jq
83 | ```
84 |
85 | ```json
86 | {
87 | "decrypted_data": "my super secret text"
88 | }
89 | ```
90 |
91 | ## change the value of PBKDF2 iterations
92 |
93 | The default value for PBKDF2 iterations is 1200000, you can change this value using the "secure_qrcode_pbkdf2_iterations" environment variable.
94 |
95 | ```
96 | docker run --rm -p 8000:8000 -e secure_qrcode_pbkdf2_iterations=1000000 quay.io/allisson/secure-qrcode
97 | ```
98 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "secure-qrcode"
3 | version = "0.1.0"
4 | description = "Encrypt your data using the modern ChaCha20-Poly1305 cipher and export it into a secure QR code"
5 | readme = "README.md"
6 | requires-python = ">=3.13,<3.14"
7 | dependencies = [
8 | "cryptography>=44.0.2",
9 | "fastapi>=0.114.2",
10 | "jinja2>=3.1.4",
11 | "pydantic-settings>=2.5.2",
12 | "qrcode[pil]>=7.4.2",
13 | "uvicorn[standard]>=0.30.6",
14 | ]
15 |
16 | [tool.uv]
17 | dev-dependencies = [
18 | "httpx>=0.27.2",
19 | "pre-commit>=3.8.0",
20 | "pytest-cov>=5.0.0",
21 | "pytest>=8.3.3",
22 | ]
23 |
24 | [tool.pytest.ini_options]
25 | minversion = "8.2"
26 | addopts = "-vvv --cov=secure_qrcode --cov-report=term-missing"
27 |
--------------------------------------------------------------------------------
/ruff.toml:
--------------------------------------------------------------------------------
1 | # Line and indent config.
2 | line-length = 110
3 | indent-width = 4
4 |
5 | # Assume Python 3.12.
6 | target-version = "py312"
7 |
8 | # Enable automatic fixes.
9 | fix = true
10 | unsafe-fixes = true
11 |
12 | # Format like black.
13 | [format]
14 | quote-style = "double"
15 | indent-style = "space"
16 | skip-magic-trailing-comma = false
17 | line-ending = "auto"
18 |
19 | # Enable linting for:
20 | # "UP": pyupgrade https://docs.astral.sh/ruff/rules/#pyupgrade-up
21 | # "F": Pyflakes https://docs.astral.sh/ruff/rules/#pyflakes-f
22 | # "E", "W": pycodestyle https://docs.astral.sh/ruff/rules/#pycodestyle-e-w
23 | # "I": isort https://docs.astral.sh/ruff/rules/#isort-i
24 | [lint]
25 | select = ["UP", "F", "E", "W", "I"]
26 | ignore = ["E203", "E501"]
27 |
28 | # Isort config.
29 | [lint.isort]
30 | section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"]
31 | order-by-type = false
32 |
--------------------------------------------------------------------------------
/secure_qrcode/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allisson/secure-qrcode/2078a597aa4735607737a6b597d03cd930c05301/secure_qrcode/__init__.py
--------------------------------------------------------------------------------
/secure_qrcode/api.py:
--------------------------------------------------------------------------------
1 | from base64 import b64encode
2 |
3 | from fastapi import FastAPI, Request
4 | from fastapi.responses import HTMLResponse, JSONResponse
5 | from fastapi.staticfiles import StaticFiles
6 | from fastapi.templating import Jinja2Templates
7 |
8 | from secure_qrcode.crypto import decrypt, encrypt
9 | from secure_qrcode.exceptions import DecryptError
10 | from secure_qrcode.models import (
11 | DecodeRequest,
12 | DecodeResponse,
13 | DecryptErrorResponse,
14 | EncodeRequest,
15 | EncodeResponse,
16 | HealthResponse,
17 | )
18 | from secure_qrcode.qrcode import make
19 |
20 | app = FastAPI(
21 | title="Secure QR code",
22 | description="Encrypt your data using the modern ChaCha20-Poly1305 cipher and export it into a secure QR code",
23 | )
24 | app.mount("/static", StaticFiles(directory="static"), name="static")
25 | templates = Jinja2Templates(directory="templates")
26 | templates.env.globals["url_for"] = app.url_path_for
27 |
28 |
29 | @app.exception_handler(DecryptError)
30 | def decrypt_error_exception_handler(request: Request, exc: DecryptError):
31 | return JSONResponse(
32 | status_code=400,
33 | content={"message": "Incorrect decryption, please check your data"},
34 | )
35 |
36 |
37 | @app.get("/", response_class=HTMLResponse, tags=["home"])
38 | def index(request: Request):
39 | return templates.TemplateResponse("index.html", {"request": request})
40 |
41 |
42 | @app.post("/v1/encode", status_code=201, tags=["api"])
43 | def encode(request: EncodeRequest) -> EncodeResponse:
44 | encrypted_data = encrypt(request.plaintext, request.key)
45 | img_io = make(
46 | encrypted_data,
47 | error_correction=request.error_correction,
48 | box_size=request.box_size,
49 | border=request.border,
50 | )
51 | return EncodeResponse(content=b64encode(img_io.getvalue()).decode("utf-8"), media_type="image/png")
52 |
53 |
54 | @app.post(
55 | "/v1/decode",
56 | status_code=201,
57 | responses={400: {"model": DecryptErrorResponse, "description": "Incorrect decryption"}},
58 | tags=["api"],
59 | )
60 | def decode(request: DecodeRequest) -> DecodeResponse:
61 | decrypted_data = decrypt(request.encrypted_data, request.key)
62 | return DecodeResponse(decrypted_data=decrypted_data)
63 |
64 |
65 | @app.get("/healthz", tags=["healthcheck"])
66 | def healthz() -> HealthResponse:
67 | return HealthResponse(success=True)
68 |
--------------------------------------------------------------------------------
/secure_qrcode/config.py:
--------------------------------------------------------------------------------
1 | from pydantic import Field
2 | from pydantic_settings import BaseSettings, SettingsConfigDict
3 |
4 |
5 | class Settings(BaseSettings):
6 | model_config = SettingsConfigDict(env_prefix="secure_qrcode_")
7 | pbkdf2_iterations: int = Field(description="PBKDF2 iterations", default=1_200_000)
8 |
9 |
10 | settings = Settings()
11 |
--------------------------------------------------------------------------------
/secure_qrcode/crypto.py:
--------------------------------------------------------------------------------
1 | import os
2 | from base64 import b64decode, b64encode
3 |
4 | from cryptography.exceptions import InvalidTag
5 | from cryptography.hazmat.primitives import hashes
6 | from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
7 | from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
8 |
9 | from secure_qrcode.config import settings
10 | from secure_qrcode.exceptions import DecryptError
11 | from secure_qrcode.models import EncryptedData
12 |
13 |
14 | def derive_key(key: str, salt: bytes, iterations: int) -> bytes:
15 | kdf = PBKDF2HMAC(
16 | algorithm=hashes.SHA256(),
17 | length=32,
18 | salt=salt,
19 | iterations=iterations,
20 | )
21 | return kdf.derive(key.encode(encoding="utf-8"))
22 |
23 |
24 | def encrypt(plaintext: str, key: str) -> EncryptedData:
25 | salt = os.urandom(16)
26 | iterations = settings.pbkdf2_iterations
27 | associated_data = os.urandom(16)
28 | nonce = os.urandom(12)
29 | derived_key = derive_key(key, salt, iterations)
30 | chacha = ChaCha20Poly1305(derived_key)
31 | ciphertext = chacha.encrypt(nonce, plaintext.encode(encoding="utf-8"), associated_data)
32 | return EncryptedData(
33 | salt=b64encode(salt).decode("utf-8"),
34 | iterations=iterations,
35 | associated_data=b64encode(associated_data).decode("utf-8"),
36 | nonce=b64encode(nonce).decode("utf-8"),
37 | ciphertext=b64encode(ciphertext).decode("utf-8"),
38 | )
39 |
40 |
41 | def decrypt(encrypted_data: EncryptedData, key: str) -> str:
42 | salt = b64decode(encrypted_data.salt)
43 |
44 | associated_data = b64decode(encrypted_data.associated_data)
45 | nonce = b64decode(encrypted_data.nonce)
46 | ciphertext = b64decode(encrypted_data.ciphertext)
47 | derived_key = derive_key(key, salt, encrypted_data.iterations)
48 | chacha = ChaCha20Poly1305(derived_key)
49 |
50 | try:
51 | plaintext = chacha.decrypt(nonce, ciphertext, associated_data)
52 | except InvalidTag:
53 | raise DecryptError("Incorrect decryption, exc=Invalid Tag")
54 | except Exception as exc:
55 | raise DecryptError(f"Incorrect decryption, exc={exc}")
56 |
57 | return plaintext.decode("utf-8")
58 |
--------------------------------------------------------------------------------
/secure_qrcode/exceptions.py:
--------------------------------------------------------------------------------
1 | class DecryptError(Exception):
2 | pass
3 |
--------------------------------------------------------------------------------
/secure_qrcode/models.py:
--------------------------------------------------------------------------------
1 | from enum import IntEnum
2 |
3 | from pydantic import BaseModel, Field
4 |
5 |
6 | class EncryptedData(BaseModel):
7 | salt: str
8 | iterations: int
9 | associated_data: str
10 | nonce: str
11 | ciphertext: str
12 |
13 |
14 | class ErrorCorrection(IntEnum):
15 | Level_L = 1
16 | Level_M = 0
17 | Level_Q = 3
18 | Level_H = 2
19 |
20 |
21 | class EncodeRequest(BaseModel):
22 | plaintext: str = Field(min_length=1, max_length=2048, description="Text to be encrypted")
23 | key: str = Field(min_length=1, max_length=32, description="Key used to encrypt the data")
24 | error_correction: ErrorCorrection = Field(
25 | default=ErrorCorrection.Level_M,
26 | description="Error correction level, possible values: 1 (About 7% or less errors can be corrected), 0 (About 15% or less errors can be corrected), 3 (About 25% or less errors can be corrected), 2 (About 30% or less errors can be corrected)",
27 | )
28 | box_size: int = Field(default=10, description="How many pixels each 'box' of the QR code is")
29 | border: int = Field(default=4, description="How many boxes thick the border should be")
30 |
31 |
32 | class EncodeResponse(BaseModel):
33 | content: str = Field(description="Image content encoded in base64")
34 | media_type: str = Field(description="The media type of the image")
35 |
36 |
37 | class DecodeRequest(BaseModel):
38 | encrypted_data: EncryptedData = Field(description="The encrypted data read from the image")
39 | key: str = Field(min_length=1, max_length=32, description="Key used to encrypt the data")
40 |
41 |
42 | class DecodeResponse(BaseModel):
43 | decrypted_data: str = Field(description="The result decrypted data")
44 |
45 |
46 | class DecryptErrorResponse(BaseModel):
47 | message: str
48 |
49 |
50 | class HealthResponse(BaseModel):
51 | success: bool
52 |
--------------------------------------------------------------------------------
/secure_qrcode/qrcode.py:
--------------------------------------------------------------------------------
1 | import json
2 | from io import BytesIO
3 |
4 | import qrcode
5 |
6 | from secure_qrcode.models import EncryptedData, ErrorCorrection
7 |
8 |
9 | def make(
10 | encrypted_data: EncryptedData,
11 | error_correction: ErrorCorrection = ErrorCorrection.Level_M,
12 | box_size: int = 10,
13 | border: int = 4,
14 | ) -> BytesIO:
15 | data = json.dumps(encrypted_data.model_dump())
16 | qr = qrcode.QRCode(
17 | version=None,
18 | error_correction=error_correction.value,
19 | box_size=box_size,
20 | border=border,
21 | )
22 | qr.add_data(data)
23 | qr.make(fit=True)
24 | img = qr.make_image()
25 | img_io = BytesIO()
26 | img.save(img_io)
27 | img_io.seek(0)
28 | return img_io
29 |
--------------------------------------------------------------------------------
/static/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allisson/secure-qrcode/2078a597aa4735607737a6b597d03cd930c05301/static/android-chrome-192x192.png
--------------------------------------------------------------------------------
/static/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allisson/secure-qrcode/2078a597aa4735607737a6b597d03cd930c05301/static/android-chrome-512x512.png
--------------------------------------------------------------------------------
/static/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allisson/secure-qrcode/2078a597aa4735607737a6b597d03cd930c05301/static/apple-touch-icon.png
--------------------------------------------------------------------------------
/static/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #da532c
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/static/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allisson/secure-qrcode/2078a597aa4735607737a6b597d03cd930c05301/static/favicon-16x16.png
--------------------------------------------------------------------------------
/static/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allisson/secure-qrcode/2078a597aa4735607737a6b597d03cd930c05301/static/favicon-32x32.png
--------------------------------------------------------------------------------
/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allisson/secure-qrcode/2078a597aa4735607737a6b597d03cd930c05301/static/favicon.ico
--------------------------------------------------------------------------------
/static/github.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/static/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allisson/secure-qrcode/2078a597aa4735607737a6b597d03cd930c05301/static/mstile-150x150.png
--------------------------------------------------------------------------------
/static/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "",
3 | "short_name": "",
4 | "icons": [
5 | {
6 | "src": "/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/android-chrome-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "theme_color": "#ffffff",
17 | "background_color": "#ffffff",
18 | "display": "standalone"
19 | }
20 |
--------------------------------------------------------------------------------
/static/styles.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | margin: 0;
4 | padding: 0;
5 | }
6 |
7 | body {
8 | background: linear-gradient(90deg, #00d2ff 0%, #3a47d5 100%);
9 | }
10 |
11 | #root {
12 | display: flex;
13 | justify-content: center;
14 | align-items: center;
15 | }
16 |
17 | main {
18 | display: flex;
19 | flex-direction: column;
20 | }
21 |
22 | .container {
23 | background-color: #e8eef2;
24 | border-radius: 2px;
25 | box-shadow: rgba(0, 0, 0, 0.16) 0px 1px 4px;
26 | padding: 0;
27 | }
28 |
29 | img {
30 | width: 300px;
31 | height: 300px;
32 | }
33 |
34 | .nav-link {
35 | text-transform: uppercase;
36 | }
37 |
38 | .nav-tabs .nav-item.show .nav-link,
39 | .nav-tabs .nav-link,
40 | .nav-tabs .nav-link.active {
41 | background-color: transparent;
42 | border: none;
43 | color: white;
44 | font-weight: bold;
45 | margin: 2px;
46 | }
47 |
48 | .nav-tabs .nav-link.active {
49 | border-bottom: 3px solid white;
50 | }
51 |
52 | .nav-tabs {
53 | border: none;
54 | }
55 |
56 | .feature {
57 | display: flex;
58 | flex-direction: row;
59 | }
60 |
61 | form {
62 | padding: 20px;
63 | max-width: 400px;
64 | width: 400px;
65 | overflow: hidden;
66 | }
67 |
68 | .qrCode {
69 | background-color: white;
70 | padding: 20px;
71 | width: 340px;
72 | }
73 |
74 | .json {
75 | background: #f2f2f2;
76 | padding: 10px;
77 | overflow: auto;
78 | /* first line not indented */
79 | & > .line {
80 | margin-left: 0;
81 | }
82 | .line {
83 | margin-left: 4px;
84 | }
85 | .key {
86 | margin-right: 2px;
87 | color: black;
88 | font-weight: 600;
89 | }
90 | .string {
91 | color: green;
92 | }
93 | .number {
94 | color: blue;
95 | }
96 | .boolean {
97 | color: purple;
98 | }
99 | .null {
100 | color: red;
101 | }
102 | }
103 |
104 | footer a {
105 | color: white;
106 | font-size: 16px;
107 | }
108 |
109 | img.icon-footer {
110 | width: 24px;
111 | height: 24px;
112 | margin-right: 10px;
113 | }
114 |
115 | .upload {
116 | display: flex;
117 | justify-content: center;
118 | align-items: center;
119 | padding: 20px 0;
120 | }
121 |
122 | .btn-methods {
123 | background: transparent !important;
124 | border: none !important;
125 | }
126 |
127 | @media only screen and (max-width: 600px) {
128 | body {
129 | padding: 8px;
130 | }
131 | form {
132 | width: auto;
133 | }
134 | .feature {
135 | flex-direction: column;
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | QRCode Generator
13 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
505 |
506 |
507 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allisson/secure-qrcode/2078a597aa4735607737a6b597d03cd930c05301/tests/__init__.py
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from fastapi.testclient import TestClient
3 |
4 | from secure_qrcode.api import app
5 |
6 |
7 | @pytest.fixture
8 | def key():
9 | return "my super secret key"
10 |
11 |
12 | @pytest.fixture
13 | def plaintext():
14 | return "super secret text"
15 |
16 |
17 | @pytest.fixture
18 | def client():
19 | return TestClient(app)
20 |
--------------------------------------------------------------------------------
/tests/test_api.py:
--------------------------------------------------------------------------------
1 | from base64 import b64encode
2 |
3 | from secure_qrcode.models import DecodeRequest, EncodeRequest, EncryptedData
4 |
5 |
6 | def test_index(client):
7 | response = client.get("/")
8 |
9 | assert response.status_code == 200
10 |
11 |
12 | def test_encode(client, plaintext, key):
13 | request = EncodeRequest(plaintext=plaintext, key=key)
14 | response = client.post("/v1/encode", json=request.model_dump())
15 |
16 | assert response.status_code == 201
17 | response_data = response.json()
18 | assert response_data["content"]
19 | assert response_data["media_type"] == "image/png"
20 |
21 |
22 | def test_decode(client, plaintext, key):
23 | encrypted_data = EncryptedData(
24 | salt="KtiCW1E0VLupOXOtpDIlZQ==",
25 | iterations=1200000,
26 | associated_data="JFPRP6/RMmCIn3DLjA/ceg==",
27 | nonce="LbF9P5FwPYyGCTJM",
28 | ciphertext="/N8WF0+QnqsDhOQ9iWuhWrXgbrZlG4Hqm9cYt/QO9Msu",
29 | )
30 | request = DecodeRequest(encrypted_data=encrypted_data, key=key)
31 | response = client.post("/v1/decode", json=request.model_dump())
32 |
33 | assert response.status_code == 201
34 | response_data = response.json()
35 | assert response_data["decrypted_data"] == plaintext
36 |
37 |
38 | def test_decode_error(client, key):
39 | encrypted_data = EncryptedData(
40 | salt="KtiCW1E0VLupOXOtpDIlZQ==",
41 | iterations=1200000,
42 | associated_data="JFPRP6/RMmCIn3DLjA/ceg==",
43 | nonce="LbF9P5FwPYyGCTJM",
44 | ciphertext="/N8WF0+QnqsDhOQ9iWuhWrXgbrZlG4Hqm9cYt/QO9Msu",
45 | )
46 | encrypted_data.associated_data = b64encode(b"invalid-aad").decode("utf-8")
47 | request = DecodeRequest(encrypted_data=encrypted_data, key=key)
48 | response = client.post("/v1/decode", json=request.model_dump())
49 |
50 | assert response.status_code == 400
51 | response_data = response.json()
52 | assert response_data["message"] == "Incorrect decryption, please check your data"
53 |
54 |
55 | def test_healthz(client):
56 | response = client.get("/healthz")
57 |
58 | assert response.status_code == 200
59 |
--------------------------------------------------------------------------------
/tests/test_crypto.py:
--------------------------------------------------------------------------------
1 | from base64 import b64encode
2 |
3 | import pytest
4 |
5 | from secure_qrcode.crypto import decrypt, encrypt
6 | from secure_qrcode.exceptions import DecryptError
7 | from secure_qrcode.models import EncryptedData
8 |
9 |
10 | def test_encrypt_decrypt(plaintext, key):
11 | encrypted_data = encrypt(plaintext, key)
12 |
13 | assert encrypted_data.salt
14 | assert encrypted_data.iterations
15 | assert encrypted_data.associated_data
16 | assert encrypted_data.nonce
17 | assert encrypted_data.ciphertext
18 | assert decrypt(encrypted_data, key) == plaintext
19 |
20 |
21 | def test_decrypt_error(key):
22 | encrypted_data = EncryptedData(
23 | salt="KtiCW1E0VLupOXOtpDIlZQ==",
24 | iterations=1200000,
25 | associated_data="JFPRP6/RMmCIn3DLjA/ceg==",
26 | nonce="LbF9P5FwPYyGCTJM",
27 | ciphertext="/N8WF0+QnqsDhOQ9iWuhWrXgbrZlG4Hqm9cYt/QO9Msu",
28 | )
29 | encrypted_data.associated_data = b64encode(b"invalid-aad").decode("utf-8")
30 |
31 | with pytest.raises(DecryptError) as excinfo:
32 | decrypt(encrypted_data, key)
33 |
34 | assert str(excinfo.value) == "Incorrect decryption, exc=Invalid Tag"
35 |
--------------------------------------------------------------------------------
/tests/test_qrcode.py:
--------------------------------------------------------------------------------
1 | from secure_qrcode.models import EncryptedData
2 | from secure_qrcode.qrcode import make
3 |
4 |
5 | def test_make():
6 | encrypted_data = EncryptedData(
7 | salt="KtiCW1E0VLupOXOtpDIlZQ==",
8 | iterations=1200000,
9 | associated_data="JFPRP6/RMmCIn3DLjA/ceg==",
10 | nonce="LbF9P5FwPYyGCTJM",
11 | ciphertext="/N8WF0+QnqsDhOQ9iWuhWrXgbrZlG4Hqm9cYt/QO9Msu",
12 | )
13 |
14 | img_io = make(encrypted_data)
15 | assert img_io.getbuffer().nbytes > 0
16 |
--------------------------------------------------------------------------------
/uv.lock:
--------------------------------------------------------------------------------
1 | version = 1
2 | requires-python = "==3.13.*"
3 |
4 | [[package]]
5 | name = "annotated-types"
6 | version = "0.7.0"
7 | source = { registry = "https://pypi.org/simple" }
8 | sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
9 | wheels = [
10 | { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
11 | ]
12 |
13 | [[package]]
14 | name = "anyio"
15 | version = "4.9.0"
16 | source = { registry = "https://pypi.org/simple" }
17 | dependencies = [
18 | { name = "idna" },
19 | { name = "sniffio" },
20 | ]
21 | sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 }
22 | wheels = [
23 | { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 },
24 | ]
25 |
26 | [[package]]
27 | name = "certifi"
28 | version = "2025.1.31"
29 | source = { registry = "https://pypi.org/simple" }
30 | sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 }
31 | wheels = [
32 | { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 },
33 | ]
34 |
35 | [[package]]
36 | name = "cffi"
37 | version = "1.17.1"
38 | source = { registry = "https://pypi.org/simple" }
39 | dependencies = [
40 | { name = "pycparser" },
41 | ]
42 | sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 }
43 | wheels = [
44 | { 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 },
45 | { 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 },
46 | { 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 },
47 | { 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 },
48 | { 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 },
49 | { 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 },
50 | { 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 },
51 | { 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 },
52 | { 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 },
53 | { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 },
54 | { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 },
55 | ]
56 |
57 | [[package]]
58 | name = "cfgv"
59 | version = "3.4.0"
60 | source = { registry = "https://pypi.org/simple" }
61 | sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 }
62 | wheels = [
63 | { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 },
64 | ]
65 |
66 | [[package]]
67 | name = "click"
68 | version = "8.1.8"
69 | source = { registry = "https://pypi.org/simple" }
70 | dependencies = [
71 | { name = "colorama", marker = "sys_platform == 'win32'" },
72 | ]
73 | sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
74 | wheels = [
75 | { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 },
76 | ]
77 |
78 | [[package]]
79 | name = "colorama"
80 | version = "0.4.6"
81 | source = { registry = "https://pypi.org/simple" }
82 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
83 | wheels = [
84 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
85 | ]
86 |
87 | [[package]]
88 | name = "coverage"
89 | version = "7.8.0"
90 | source = { registry = "https://pypi.org/simple" }
91 | sdist = { url = "https://files.pythonhosted.org/packages/19/4f/2251e65033ed2ce1e68f00f91a0294e0f80c80ae8c3ebbe2f12828c4cd53/coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501", size = 811872 }
92 | wheels = [
93 | { url = "https://files.pythonhosted.org/packages/f3/21/87e9b97b568e223f3438d93072479c2f36cc9b3f6b9f7094b9d50232acc0/coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd", size = 211708 },
94 | { url = "https://files.pythonhosted.org/packages/75/be/882d08b28a0d19c9c4c2e8a1c6ebe1f79c9c839eb46d4fca3bd3b34562b9/coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00", size = 211981 },
95 | { url = "https://files.pythonhosted.org/packages/7a/1d/ce99612ebd58082fbe3f8c66f6d8d5694976c76a0d474503fa70633ec77f/coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64", size = 245495 },
96 | { url = "https://files.pythonhosted.org/packages/dc/8d/6115abe97df98db6b2bd76aae395fcc941d039a7acd25f741312ced9a78f/coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067", size = 242538 },
97 | { url = "https://files.pythonhosted.org/packages/cb/74/2f8cc196643b15bc096d60e073691dadb3dca48418f08bc78dd6e899383e/coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008", size = 244561 },
98 | { url = "https://files.pythonhosted.org/packages/22/70/c10c77cd77970ac965734fe3419f2c98665f6e982744a9bfb0e749d298f4/coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733", size = 244633 },
99 | { url = "https://files.pythonhosted.org/packages/38/5a/4f7569d946a07c952688debee18c2bb9ab24f88027e3d71fd25dbc2f9dca/coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323", size = 242712 },
100 | { url = "https://files.pythonhosted.org/packages/bb/a1/03a43b33f50475a632a91ea8c127f7e35e53786dbe6781c25f19fd5a65f8/coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3", size = 244000 },
101 | { url = "https://files.pythonhosted.org/packages/6a/89/ab6c43b1788a3128e4d1b7b54214548dcad75a621f9d277b14d16a80d8a1/coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d", size = 214195 },
102 | { url = "https://files.pythonhosted.org/packages/12/12/6bf5f9a8b063d116bac536a7fb594fc35cb04981654cccb4bbfea5dcdfa0/coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487", size = 214998 },
103 | { url = "https://files.pythonhosted.org/packages/2a/e6/1e9df74ef7a1c983a9c7443dac8aac37a46f1939ae3499424622e72a6f78/coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25", size = 212541 },
104 | { url = "https://files.pythonhosted.org/packages/04/51/c32174edb7ee49744e2e81c4b1414ac9df3dacfcb5b5f273b7f285ad43f6/coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42", size = 212767 },
105 | { url = "https://files.pythonhosted.org/packages/e9/8f/f454cbdb5212f13f29d4a7983db69169f1937e869a5142bce983ded52162/coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502", size = 256997 },
106 | { url = "https://files.pythonhosted.org/packages/e6/74/2bf9e78b321216d6ee90a81e5c22f912fc428442c830c4077b4a071db66f/coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1", size = 252708 },
107 | { url = "https://files.pythonhosted.org/packages/92/4d/50d7eb1e9a6062bee6e2f92e78b0998848a972e9afad349b6cdde6fa9e32/coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4", size = 255046 },
108 | { url = "https://files.pythonhosted.org/packages/40/9e/71fb4e7402a07c4198ab44fc564d09d7d0ffca46a9fb7b0a7b929e7641bd/coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73", size = 256139 },
109 | { url = "https://files.pythonhosted.org/packages/49/1a/78d37f7a42b5beff027e807c2843185961fdae7fe23aad5a4837c93f9d25/coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a", size = 254307 },
110 | { url = "https://files.pythonhosted.org/packages/58/e9/8fb8e0ff6bef5e170ee19d59ca694f9001b2ec085dc99b4f65c128bb3f9a/coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883", size = 255116 },
111 | { url = "https://files.pythonhosted.org/packages/56/b0/d968ecdbe6fe0a863de7169bbe9e8a476868959f3af24981f6a10d2b6924/coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada", size = 214909 },
112 | { url = "https://files.pythonhosted.org/packages/87/e9/d6b7ef9fecf42dfb418d93544af47c940aa83056c49e6021a564aafbc91f/coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257", size = 216068 },
113 | { url = "https://files.pythonhosted.org/packages/59/f1/4da7717f0063a222db253e7121bd6a56f6fb1ba439dcc36659088793347c/coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7", size = 203435 },
114 | ]
115 |
116 | [[package]]
117 | name = "cryptography"
118 | version = "44.0.2"
119 | source = { registry = "https://pypi.org/simple" }
120 | dependencies = [
121 | { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
122 | ]
123 | sdist = { url = "https://files.pythonhosted.org/packages/cd/25/4ce80c78963834b8a9fd1cc1266be5ed8d1840785c0f2e1b73b8d128d505/cryptography-44.0.2.tar.gz", hash = "sha256:c63454aa261a0cf0c5b4718349629793e9e634993538db841165b3df74f37ec0", size = 710807 }
124 | wheels = [
125 | { url = "https://files.pythonhosted.org/packages/92/ef/83e632cfa801b221570c5f58c0369db6fa6cef7d9ff859feab1aae1a8a0f/cryptography-44.0.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:efcfe97d1b3c79e486554efddeb8f6f53a4cdd4cf6086642784fa31fc384e1d7", size = 6676361 },
126 | { url = "https://files.pythonhosted.org/packages/30/ec/7ea7c1e4c8fc8329506b46c6c4a52e2f20318425d48e0fe597977c71dbce/cryptography-44.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29ecec49f3ba3f3849362854b7253a9f59799e3763b0c9d0826259a88efa02f1", size = 3952350 },
127 | { url = "https://files.pythonhosted.org/packages/27/61/72e3afdb3c5ac510330feba4fc1faa0fe62e070592d6ad00c40bb69165e5/cryptography-44.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc821e161ae88bfe8088d11bb39caf2916562e0a2dc7b6d56714a48b784ef0bb", size = 4166572 },
128 | { url = "https://files.pythonhosted.org/packages/26/e4/ba680f0b35ed4a07d87f9e98f3ebccb05091f3bf6b5a478b943253b3bbd5/cryptography-44.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3c00b6b757b32ce0f62c574b78b939afab9eecaf597c4d624caca4f9e71e7843", size = 3958124 },
129 | { url = "https://files.pythonhosted.org/packages/9c/e8/44ae3e68c8b6d1cbc59040288056df2ad7f7f03bbcaca6b503c737ab8e73/cryptography-44.0.2-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7bdcd82189759aba3816d1f729ce42ffded1ac304c151d0a8e89b9996ab863d5", size = 3678122 },
130 | { url = "https://files.pythonhosted.org/packages/27/7b/664ea5e0d1eab511a10e480baf1c5d3e681c7d91718f60e149cec09edf01/cryptography-44.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4973da6ca3db4405c54cd0b26d328be54c7747e89e284fcff166132eb7bccc9c", size = 4191831 },
131 | { url = "https://files.pythonhosted.org/packages/2a/07/79554a9c40eb11345e1861f46f845fa71c9e25bf66d132e123d9feb8e7f9/cryptography-44.0.2-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4e389622b6927d8133f314949a9812972711a111d577a5d1f4bee5e58736b80a", size = 3960583 },
132 | { url = "https://files.pythonhosted.org/packages/bb/6d/858e356a49a4f0b591bd6789d821427de18432212e137290b6d8a817e9bf/cryptography-44.0.2-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f514ef4cd14bb6fb484b4a60203e912cfcb64f2ab139e88c2274511514bf7308", size = 4191753 },
133 | { url = "https://files.pythonhosted.org/packages/b2/80/62df41ba4916067fa6b125aa8c14d7e9181773f0d5d0bd4dcef580d8b7c6/cryptography-44.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1bc312dfb7a6e5d66082c87c34c8a62176e684b6fe3d90fcfe1568de675e6688", size = 4079550 },
134 | { url = "https://files.pythonhosted.org/packages/f3/cd/2558cc08f7b1bb40683f99ff4327f8dcfc7de3affc669e9065e14824511b/cryptography-44.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b721b8b4d948b218c88cb8c45a01793483821e709afe5f622861fc6182b20a7", size = 4298367 },
135 | { url = "https://files.pythonhosted.org/packages/71/59/94ccc74788945bc3bd4cf355d19867e8057ff5fdbcac781b1ff95b700fb1/cryptography-44.0.2-cp37-abi3-win32.whl", hash = "sha256:51e4de3af4ec3899d6d178a8c005226491c27c4ba84101bfb59c901e10ca9f79", size = 2772843 },
136 | { url = "https://files.pythonhosted.org/packages/ca/2c/0d0bbaf61ba05acb32f0841853cfa33ebb7a9ab3d9ed8bb004bd39f2da6a/cryptography-44.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:c505d61b6176aaf982c5717ce04e87da5abc9a36a5b39ac03905c4aafe8de7aa", size = 3209057 },
137 | { url = "https://files.pythonhosted.org/packages/9e/be/7a26142e6d0f7683d8a382dd963745e65db895a79a280a30525ec92be890/cryptography-44.0.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8e0ddd63e6bf1161800592c71ac794d3fb8001f2caebe0966e77c5234fa9efc3", size = 6677789 },
138 | { url = "https://files.pythonhosted.org/packages/06/88/638865be7198a84a7713950b1db7343391c6066a20e614f8fa286eb178ed/cryptography-44.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81276f0ea79a208d961c433a947029e1a15948966658cf6710bbabb60fcc2639", size = 3951919 },
139 | { url = "https://files.pythonhosted.org/packages/d7/fc/99fe639bcdf58561dfad1faa8a7369d1dc13f20acd78371bb97a01613585/cryptography-44.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1e657c0f4ea2a23304ee3f964db058c9e9e635cc7019c4aa21c330755ef6fd", size = 4167812 },
140 | { url = "https://files.pythonhosted.org/packages/53/7b/aafe60210ec93d5d7f552592a28192e51d3c6b6be449e7fd0a91399b5d07/cryptography-44.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6210c05941994290f3f7f175a4a57dbbb2afd9273657614c506d5976db061181", size = 3958571 },
141 | { url = "https://files.pythonhosted.org/packages/16/32/051f7ce79ad5a6ef5e26a92b37f172ee2d6e1cce09931646eef8de1e9827/cryptography-44.0.2-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1c3572526997b36f245a96a2b1713bf79ce99b271bbcf084beb6b9b075f29ea", size = 3679832 },
142 | { url = "https://files.pythonhosted.org/packages/78/2b/999b2a1e1ba2206f2d3bca267d68f350beb2b048a41ea827e08ce7260098/cryptography-44.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b042d2a275c8cee83a4b7ae30c45a15e6a4baa65a179a0ec2d78ebb90e4f6699", size = 4193719 },
143 | { url = "https://files.pythonhosted.org/packages/72/97/430e56e39a1356e8e8f10f723211a0e256e11895ef1a135f30d7d40f2540/cryptography-44.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d03806036b4f89e3b13b6218fefea8d5312e450935b1a2d55f0524e2ed7c59d9", size = 3960852 },
144 | { url = "https://files.pythonhosted.org/packages/89/33/c1cf182c152e1d262cac56850939530c05ca6c8d149aa0dcee490b417e99/cryptography-44.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c7362add18b416b69d58c910caa217f980c5ef39b23a38a0880dfd87bdf8cd23", size = 4193906 },
145 | { url = "https://files.pythonhosted.org/packages/e1/99/87cf26d4f125380dc674233971069bc28d19b07f7755b29861570e513650/cryptography-44.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8cadc6e3b5a1f144a039ea08a0bdb03a2a92e19c46be3285123d32029f40a922", size = 4081572 },
146 | { url = "https://files.pythonhosted.org/packages/b3/9f/6a3e0391957cc0c5f84aef9fbdd763035f2b52e998a53f99345e3ac69312/cryptography-44.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f101b1f780f7fc613d040ca4bdf835c6ef3b00e9bd7125a4255ec574c7916e4", size = 4298631 },
147 | { url = "https://files.pythonhosted.org/packages/e2/a5/5bc097adb4b6d22a24dea53c51f37e480aaec3465285c253098642696423/cryptography-44.0.2-cp39-abi3-win32.whl", hash = "sha256:3dc62975e31617badc19a906481deacdeb80b4bb454394b4098e3f2525a488c5", size = 2773792 },
148 | { url = "https://files.pythonhosted.org/packages/33/cf/1f7649b8b9a3543e042d3f348e398a061923ac05b507f3f4d95f11938aa9/cryptography-44.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:5f6f90b72d8ccadb9c6e311c775c8305381db88374c65fa1a68250aa8a9cb3a6", size = 3210957 },
149 | ]
150 |
151 | [[package]]
152 | name = "distlib"
153 | version = "0.3.9"
154 | source = { registry = "https://pypi.org/simple" }
155 | sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 }
156 | wheels = [
157 | { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 },
158 | ]
159 |
160 | [[package]]
161 | name = "fastapi"
162 | version = "0.115.12"
163 | source = { registry = "https://pypi.org/simple" }
164 | dependencies = [
165 | { name = "pydantic" },
166 | { name = "starlette" },
167 | { name = "typing-extensions" },
168 | ]
169 | sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236 }
170 | wheels = [
171 | { url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164 },
172 | ]
173 |
174 | [[package]]
175 | name = "filelock"
176 | version = "3.18.0"
177 | source = { registry = "https://pypi.org/simple" }
178 | sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075 }
179 | wheels = [
180 | { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215 },
181 | ]
182 |
183 | [[package]]
184 | name = "h11"
185 | version = "0.14.0"
186 | source = { registry = "https://pypi.org/simple" }
187 | sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 }
188 | wheels = [
189 | { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 },
190 | ]
191 |
192 | [[package]]
193 | name = "httpcore"
194 | version = "1.0.7"
195 | source = { registry = "https://pypi.org/simple" }
196 | dependencies = [
197 | { name = "certifi" },
198 | { name = "h11" },
199 | ]
200 | sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 }
201 | wheels = [
202 | { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 },
203 | ]
204 |
205 | [[package]]
206 | name = "httptools"
207 | version = "0.6.4"
208 | source = { registry = "https://pypi.org/simple" }
209 | sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639 }
210 | wheels = [
211 | { url = "https://files.pythonhosted.org/packages/94/a3/9fe9ad23fd35f7de6b91eeb60848986058bd8b5a5c1e256f5860a160cc3e/httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660", size = 197214 },
212 | { url = "https://files.pythonhosted.org/packages/ea/d9/82d5e68bab783b632023f2fa31db20bebb4e89dfc4d2293945fd68484ee4/httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083", size = 102431 },
213 | { url = "https://files.pythonhosted.org/packages/96/c1/cb499655cbdbfb57b577734fde02f6fa0bbc3fe9fb4d87b742b512908dff/httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3", size = 473121 },
214 | { url = "https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071", size = 473805 },
215 | { url = "https://files.pythonhosted.org/packages/8a/0a/0d4df132bfca1507114198b766f1737d57580c9ad1cf93c1ff673e3387be/httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5", size = 448858 },
216 | { url = "https://files.pythonhosted.org/packages/1e/6a/787004fdef2cabea27bad1073bf6a33f2437b4dbd3b6fb4a9d71172b1c7c/httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0", size = 452042 },
217 | { url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8", size = 87682 },
218 | ]
219 |
220 | [[package]]
221 | name = "httpx"
222 | version = "0.28.1"
223 | source = { registry = "https://pypi.org/simple" }
224 | dependencies = [
225 | { name = "anyio" },
226 | { name = "certifi" },
227 | { name = "httpcore" },
228 | { name = "idna" },
229 | ]
230 | sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 }
231 | wheels = [
232 | { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 },
233 | ]
234 |
235 | [[package]]
236 | name = "identify"
237 | version = "2.6.9"
238 | source = { registry = "https://pypi.org/simple" }
239 | sdist = { url = "https://files.pythonhosted.org/packages/9b/98/a71ab060daec766acc30fb47dfca219d03de34a70d616a79a38c6066c5bf/identify-2.6.9.tar.gz", hash = "sha256:d40dfe3142a1421d8518e3d3985ef5ac42890683e32306ad614a29490abeb6bf", size = 99249 }
240 | wheels = [
241 | { url = "https://files.pythonhosted.org/packages/07/ce/0845144ed1f0e25db5e7a79c2354c1da4b5ce392b8966449d5db8dca18f1/identify-2.6.9-py2.py3-none-any.whl", hash = "sha256:c98b4322da415a8e5a70ff6e51fbc2d2932c015532d77e9f8537b4ba7813b150", size = 99101 },
242 | ]
243 |
244 | [[package]]
245 | name = "idna"
246 | version = "3.10"
247 | source = { registry = "https://pypi.org/simple" }
248 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
249 | wheels = [
250 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
251 | ]
252 |
253 | [[package]]
254 | name = "iniconfig"
255 | version = "2.1.0"
256 | source = { registry = "https://pypi.org/simple" }
257 | sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 }
258 | wheels = [
259 | { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 },
260 | ]
261 |
262 | [[package]]
263 | name = "jinja2"
264 | version = "3.1.6"
265 | source = { registry = "https://pypi.org/simple" }
266 | dependencies = [
267 | { name = "markupsafe" },
268 | ]
269 | sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 }
270 | wheels = [
271 | { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 },
272 | ]
273 |
274 | [[package]]
275 | name = "markupsafe"
276 | version = "3.0.2"
277 | source = { registry = "https://pypi.org/simple" }
278 | sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 }
279 | wheels = [
280 | { 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 },
281 | { 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 },
282 | { 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 },
283 | { 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 },
284 | { 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 },
285 | { 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 },
286 | { 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 },
287 | { 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 },
288 | { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 },
289 | { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 },
290 | { 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 },
291 | { 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 },
292 | { 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 },
293 | { 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 },
294 | { 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 },
295 | { 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 },
296 | { 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 },
297 | { 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 },
298 | { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 },
299 | { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 },
300 | ]
301 |
302 | [[package]]
303 | name = "nodeenv"
304 | version = "1.9.1"
305 | source = { registry = "https://pypi.org/simple" }
306 | sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 }
307 | wheels = [
308 | { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 },
309 | ]
310 |
311 | [[package]]
312 | name = "packaging"
313 | version = "24.2"
314 | source = { registry = "https://pypi.org/simple" }
315 | sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
316 | wheels = [
317 | { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
318 | ]
319 |
320 | [[package]]
321 | name = "pillow"
322 | version = "11.1.0"
323 | source = { registry = "https://pypi.org/simple" }
324 | sdist = { url = "https://files.pythonhosted.org/packages/f3/af/c097e544e7bd278333db77933e535098c259609c4eb3b85381109602fb5b/pillow-11.1.0.tar.gz", hash = "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20", size = 46742715 }
325 | wheels = [
326 | { url = "https://files.pythonhosted.org/packages/b3/31/9ca79cafdce364fd5c980cd3416c20ce1bebd235b470d262f9d24d810184/pillow-11.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc", size = 3226640 },
327 | { url = "https://files.pythonhosted.org/packages/ac/0f/ff07ad45a1f172a497aa393b13a9d81a32e1477ef0e869d030e3c1532521/pillow-11.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0", size = 3101437 },
328 | { url = "https://files.pythonhosted.org/packages/08/2f/9906fca87a68d29ec4530be1f893149e0cb64a86d1f9f70a7cfcdfe8ae44/pillow-11.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1", size = 4326605 },
329 | { url = "https://files.pythonhosted.org/packages/b0/0f/f3547ee15b145bc5c8b336401b2d4c9d9da67da9dcb572d7c0d4103d2c69/pillow-11.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec", size = 4411173 },
330 | { url = "https://files.pythonhosted.org/packages/b1/df/bf8176aa5db515c5de584c5e00df9bab0713548fd780c82a86cba2c2fedb/pillow-11.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5", size = 4369145 },
331 | { url = "https://files.pythonhosted.org/packages/de/7c/7433122d1cfadc740f577cb55526fdc39129a648ac65ce64db2eb7209277/pillow-11.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114", size = 4496340 },
332 | { url = "https://files.pythonhosted.org/packages/25/46/dd94b93ca6bd555588835f2504bd90c00d5438fe131cf01cfa0c5131a19d/pillow-11.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352", size = 4296906 },
333 | { url = "https://files.pythonhosted.org/packages/a8/28/2f9d32014dfc7753e586db9add35b8a41b7a3b46540e965cb6d6bc607bd2/pillow-11.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3", size = 4431759 },
334 | { url = "https://files.pythonhosted.org/packages/33/48/19c2cbe7403870fbe8b7737d19eb013f46299cdfe4501573367f6396c775/pillow-11.1.0-cp313-cp313-win32.whl", hash = "sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9", size = 2291657 },
335 | { url = "https://files.pythonhosted.org/packages/3b/ad/285c556747d34c399f332ba7c1a595ba245796ef3e22eae190f5364bb62b/pillow-11.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c", size = 2626304 },
336 | { url = "https://files.pythonhosted.org/packages/e5/7b/ef35a71163bf36db06e9c8729608f78dedf032fc8313d19bd4be5c2588f3/pillow-11.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65", size = 2375117 },
337 | { url = "https://files.pythonhosted.org/packages/79/30/77f54228401e84d6791354888549b45824ab0ffde659bafa67956303a09f/pillow-11.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861", size = 3230060 },
338 | { url = "https://files.pythonhosted.org/packages/ce/b1/56723b74b07dd64c1010fee011951ea9c35a43d8020acd03111f14298225/pillow-11.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081", size = 3106192 },
339 | { url = "https://files.pythonhosted.org/packages/e1/cd/7bf7180e08f80a4dcc6b4c3a0aa9e0b0ae57168562726a05dc8aa8fa66b0/pillow-11.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c", size = 4446805 },
340 | { url = "https://files.pythonhosted.org/packages/97/42/87c856ea30c8ed97e8efbe672b58c8304dee0573f8c7cab62ae9e31db6ae/pillow-11.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547", size = 4530623 },
341 | { url = "https://files.pythonhosted.org/packages/ff/41/026879e90c84a88e33fb00cc6bd915ac2743c67e87a18f80270dfe3c2041/pillow-11.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab", size = 4465191 },
342 | { url = "https://files.pythonhosted.org/packages/e5/fb/a7960e838bc5df57a2ce23183bfd2290d97c33028b96bde332a9057834d3/pillow-11.1.0-cp313-cp313t-win32.whl", hash = "sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9", size = 2295494 },
343 | { url = "https://files.pythonhosted.org/packages/d7/6c/6ec83ee2f6f0fda8d4cf89045c6be4b0373ebfc363ba8538f8c999f63fcd/pillow-11.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe", size = 2631595 },
344 | { url = "https://files.pythonhosted.org/packages/cf/6c/41c21c6c8af92b9fea313aa47c75de49e2f9a467964ee33eb0135d47eb64/pillow-11.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756", size = 2377651 },
345 | ]
346 |
347 | [[package]]
348 | name = "platformdirs"
349 | version = "4.3.7"
350 | source = { registry = "https://pypi.org/simple" }
351 | sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291 }
352 | wheels = [
353 | { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499 },
354 | ]
355 |
356 | [[package]]
357 | name = "pluggy"
358 | version = "1.5.0"
359 | source = { registry = "https://pypi.org/simple" }
360 | sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 }
361 | wheels = [
362 | { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 },
363 | ]
364 |
365 | [[package]]
366 | name = "pre-commit"
367 | version = "4.2.0"
368 | source = { registry = "https://pypi.org/simple" }
369 | dependencies = [
370 | { name = "cfgv" },
371 | { name = "identify" },
372 | { name = "nodeenv" },
373 | { name = "pyyaml" },
374 | { name = "virtualenv" },
375 | ]
376 | sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424 }
377 | wheels = [
378 | { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707 },
379 | ]
380 |
381 | [[package]]
382 | name = "pycparser"
383 | version = "2.22"
384 | source = { registry = "https://pypi.org/simple" }
385 | sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 }
386 | wheels = [
387 | { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 },
388 | ]
389 |
390 | [[package]]
391 | name = "pydantic"
392 | version = "2.11.2"
393 | source = { registry = "https://pypi.org/simple" }
394 | dependencies = [
395 | { name = "annotated-types" },
396 | { name = "pydantic-core" },
397 | { name = "typing-extensions" },
398 | { name = "typing-inspection" },
399 | ]
400 | sdist = { url = "https://files.pythonhosted.org/packages/b0/41/832125a41fe098b58d1fdd04ae819b4dc6b34d6b09ed78304fd93d4bc051/pydantic-2.11.2.tar.gz", hash = "sha256:2138628e050bd7a1e70b91d4bf4a91167f4ad76fdb83209b107c8d84b854917e", size = 784742 }
401 | wheels = [
402 | { url = "https://files.pythonhosted.org/packages/bf/c2/0f3baea344d0b15e35cb3e04ad5b953fa05106b76efbf4c782a3f47f22f5/pydantic-2.11.2-py3-none-any.whl", hash = "sha256:7f17d25846bcdf89b670a86cdfe7b29a9f1c9ca23dee154221c9aa81845cfca7", size = 443295 },
403 | ]
404 |
405 | [[package]]
406 | name = "pydantic-core"
407 | version = "2.33.1"
408 | source = { registry = "https://pypi.org/simple" }
409 | dependencies = [
410 | { name = "typing-extensions" },
411 | ]
412 | sdist = { url = "https://files.pythonhosted.org/packages/17/19/ed6a078a5287aea7922de6841ef4c06157931622c89c2a47940837b5eecd/pydantic_core-2.33.1.tar.gz", hash = "sha256:bcc9c6fdb0ced789245b02b7d6603e17d1563064ddcfc36f046b61c0c05dd9df", size = 434395 }
413 | wheels = [
414 | { url = "https://files.pythonhosted.org/packages/7a/24/eed3466a4308d79155f1cdd5c7432c80ddcc4530ba8623b79d5ced021641/pydantic_core-2.33.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:70af6a21237b53d1fe7b9325b20e65cbf2f0a848cf77bed492b029139701e66a", size = 2033551 },
415 | { url = "https://files.pythonhosted.org/packages/ab/14/df54b1a0bc9b6ded9b758b73139d2c11b4e8eb43e8ab9c5847c0a2913ada/pydantic_core-2.33.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:282b3fe1bbbe5ae35224a0dbd05aed9ccabccd241e8e6b60370484234b456266", size = 1852785 },
416 | { url = "https://files.pythonhosted.org/packages/fa/96/e275f15ff3d34bb04b0125d9bc8848bf69f25d784d92a63676112451bfb9/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b315e596282bbb5822d0c7ee9d255595bd7506d1cb20c2911a4da0b970187d3", size = 1897758 },
417 | { url = "https://files.pythonhosted.org/packages/b7/d8/96bc536e975b69e3a924b507d2a19aedbf50b24e08c80fb00e35f9baaed8/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1dfae24cf9921875ca0ca6a8ecb4bb2f13c855794ed0d468d6abbec6e6dcd44a", size = 1986109 },
418 | { url = "https://files.pythonhosted.org/packages/90/72/ab58e43ce7e900b88cb571ed057b2fcd0e95b708a2e0bed475b10130393e/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dd8ecfde08d8bfadaea669e83c63939af76f4cf5538a72597016edfa3fad516", size = 2129159 },
419 | { url = "https://files.pythonhosted.org/packages/dc/3f/52d85781406886c6870ac995ec0ba7ccc028b530b0798c9080531b409fdb/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f593494876eae852dc98c43c6f260f45abdbfeec9e4324e31a481d948214764", size = 2680222 },
420 | { url = "https://files.pythonhosted.org/packages/f4/56/6e2ef42f363a0eec0fd92f74a91e0ac48cd2e49b695aac1509ad81eee86a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:948b73114f47fd7016088e5186d13faf5e1b2fe83f5e320e371f035557fd264d", size = 2006980 },
421 | { url = "https://files.pythonhosted.org/packages/4c/c0/604536c4379cc78359f9ee0aa319f4aedf6b652ec2854953f5a14fc38c5a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e11f3864eb516af21b01e25fac915a82e9ddad3bb0fb9e95a246067398b435a4", size = 2120840 },
422 | { url = "https://files.pythonhosted.org/packages/1f/46/9eb764814f508f0edfb291a0f75d10854d78113fa13900ce13729aaec3ae/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:549150be302428b56fdad0c23c2741dcdb5572413776826c965619a25d9c6bde", size = 2072518 },
423 | { url = "https://files.pythonhosted.org/packages/42/e3/fb6b2a732b82d1666fa6bf53e3627867ea3131c5f39f98ce92141e3e3dc1/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:495bc156026efafd9ef2d82372bd38afce78ddd82bf28ef5276c469e57c0c83e", size = 2248025 },
424 | { url = "https://files.pythonhosted.org/packages/5c/9d/fbe8fe9d1aa4dac88723f10a921bc7418bd3378a567cb5e21193a3c48b43/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ec79de2a8680b1a67a07490bddf9636d5c2fab609ba8c57597e855fa5fa4dacd", size = 2254991 },
425 | { url = "https://files.pythonhosted.org/packages/aa/99/07e2237b8a66438d9b26482332cda99a9acccb58d284af7bc7c946a42fd3/pydantic_core-2.33.1-cp313-cp313-win32.whl", hash = "sha256:ee12a7be1742f81b8a65b36c6921022301d466b82d80315d215c4c691724986f", size = 1915262 },
426 | { url = "https://files.pythonhosted.org/packages/8a/f4/e457a7849beeed1e5defbcf5051c6f7b3c91a0624dd31543a64fc9adcf52/pydantic_core-2.33.1-cp313-cp313-win_amd64.whl", hash = "sha256:ede9b407e39949d2afc46385ce6bd6e11588660c26f80576c11c958e6647bc40", size = 1956626 },
427 | { url = "https://files.pythonhosted.org/packages/20/d0/e8d567a7cff7b04e017ae164d98011f1e1894269fe8e90ea187a3cbfb562/pydantic_core-2.33.1-cp313-cp313-win_arm64.whl", hash = "sha256:aa687a23d4b7871a00e03ca96a09cad0f28f443690d300500603bd0adba4b523", size = 1909590 },
428 | { url = "https://files.pythonhosted.org/packages/ef/fd/24ea4302d7a527d672c5be06e17df16aabfb4e9fdc6e0b345c21580f3d2a/pydantic_core-2.33.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:401d7b76e1000d0dd5538e6381d28febdcacb097c8d340dde7d7fc6e13e9f95d", size = 1812963 },
429 | { url = "https://files.pythonhosted.org/packages/5f/95/4fbc2ecdeb5c1c53f1175a32d870250194eb2fdf6291b795ab08c8646d5d/pydantic_core-2.33.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aeb055a42d734c0255c9e489ac67e75397d59c6fbe60d155851e9782f276a9c", size = 1986896 },
430 | { url = "https://files.pythonhosted.org/packages/71/ae/fe31e7f4a62431222d8f65a3bd02e3fa7e6026d154a00818e6d30520ea77/pydantic_core-2.33.1-cp313-cp313t-win_amd64.whl", hash = "sha256:338ea9b73e6e109f15ab439e62cb3b78aa752c7fd9536794112e14bee02c8d18", size = 1931810 },
431 | ]
432 |
433 | [[package]]
434 | name = "pydantic-settings"
435 | version = "2.8.1"
436 | source = { registry = "https://pypi.org/simple" }
437 | dependencies = [
438 | { name = "pydantic" },
439 | { name = "python-dotenv" },
440 | ]
441 | sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550 }
442 | wheels = [
443 | { url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839 },
444 | ]
445 |
446 | [[package]]
447 | name = "pytest"
448 | version = "8.3.5"
449 | source = { registry = "https://pypi.org/simple" }
450 | dependencies = [
451 | { name = "colorama", marker = "sys_platform == 'win32'" },
452 | { name = "iniconfig" },
453 | { name = "packaging" },
454 | { name = "pluggy" },
455 | ]
456 | sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 }
457 | wheels = [
458 | { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 },
459 | ]
460 |
461 | [[package]]
462 | name = "pytest-cov"
463 | version = "6.1.1"
464 | source = { registry = "https://pypi.org/simple" }
465 | dependencies = [
466 | { name = "coverage" },
467 | { name = "pytest" },
468 | ]
469 | sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857 }
470 | wheels = [
471 | { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841 },
472 | ]
473 |
474 | [[package]]
475 | name = "python-dotenv"
476 | version = "1.1.0"
477 | source = { registry = "https://pypi.org/simple" }
478 | sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 }
479 | wheels = [
480 | { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 },
481 | ]
482 |
483 | [[package]]
484 | name = "pyyaml"
485 | version = "6.0.2"
486 | source = { registry = "https://pypi.org/simple" }
487 | sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 }
488 | wheels = [
489 | { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 },
490 | { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 },
491 | { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 },
492 | { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 },
493 | { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 },
494 | { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 },
495 | { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 },
496 | { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 },
497 | { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 },
498 | ]
499 |
500 | [[package]]
501 | name = "qrcode"
502 | version = "8.1"
503 | source = { registry = "https://pypi.org/simple" }
504 | dependencies = [
505 | { name = "colorama", marker = "sys_platform == 'win32'" },
506 | ]
507 | sdist = { url = "https://files.pythonhosted.org/packages/61/d4/d222d00f65c81945b55e8f64011c33cb11a2931957ba3e2845fb0874fffe/qrcode-8.1.tar.gz", hash = "sha256:e8df73caf72c3bace3e93d9fa0af5aa78267d4f3f5bc7ab1b208f271605a5e48", size = 41549 }
508 | wheels = [
509 | { url = "https://files.pythonhosted.org/packages/29/e6/273de1f5cda537b00bc2947082be747f1d76358db8b945f3a60837bcd0f6/qrcode-8.1-py3-none-any.whl", hash = "sha256:9beba317d793ab8b3838c52af72e603b8ad2599c4e9bbd5c3da37c7dcc13c5cf", size = 45711 },
510 | ]
511 |
512 | [package.optional-dependencies]
513 | pil = [
514 | { name = "pillow" },
515 | ]
516 |
517 | [[package]]
518 | name = "secure-qrcode"
519 | version = "0.1.0"
520 | source = { virtual = "." }
521 | dependencies = [
522 | { name = "cryptography" },
523 | { name = "fastapi" },
524 | { name = "jinja2" },
525 | { name = "pydantic-settings" },
526 | { name = "qrcode", extra = ["pil"] },
527 | { name = "uvicorn", extra = ["standard"] },
528 | ]
529 |
530 | [package.dev-dependencies]
531 | dev = [
532 | { name = "httpx" },
533 | { name = "pre-commit" },
534 | { name = "pytest" },
535 | { name = "pytest-cov" },
536 | ]
537 |
538 | [package.metadata]
539 | requires-dist = [
540 | { name = "cryptography", specifier = ">=44.0.2" },
541 | { name = "fastapi", specifier = ">=0.114.2" },
542 | { name = "jinja2", specifier = ">=3.1.4" },
543 | { name = "pydantic-settings", specifier = ">=2.5.2" },
544 | { name = "qrcode", extras = ["pil"], specifier = ">=7.4.2" },
545 | { name = "uvicorn", extras = ["standard"], specifier = ">=0.30.6" },
546 | ]
547 |
548 | [package.metadata.requires-dev]
549 | dev = [
550 | { name = "httpx", specifier = ">=0.27.2" },
551 | { name = "pre-commit", specifier = ">=3.8.0" },
552 | { name = "pytest", specifier = ">=8.3.3" },
553 | { name = "pytest-cov", specifier = ">=5.0.0" },
554 | ]
555 |
556 | [[package]]
557 | name = "sniffio"
558 | version = "1.3.1"
559 | source = { registry = "https://pypi.org/simple" }
560 | sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
561 | wheels = [
562 | { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
563 | ]
564 |
565 | [[package]]
566 | name = "starlette"
567 | version = "0.46.1"
568 | source = { registry = "https://pypi.org/simple" }
569 | dependencies = [
570 | { name = "anyio" },
571 | ]
572 | sdist = { url = "https://files.pythonhosted.org/packages/04/1b/52b27f2e13ceedc79a908e29eac426a63465a1a01248e5f24aa36a62aeb3/starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230", size = 2580102 }
573 | wheels = [
574 | { url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995 },
575 | ]
576 |
577 | [[package]]
578 | name = "typing-extensions"
579 | version = "4.13.1"
580 | source = { registry = "https://pypi.org/simple" }
581 | sdist = { url = "https://files.pythonhosted.org/packages/76/ad/cd3e3465232ec2416ae9b983f27b9e94dc8171d56ac99b345319a9475967/typing_extensions-4.13.1.tar.gz", hash = "sha256:98795af00fb9640edec5b8e31fc647597b4691f099ad75f469a2616be1a76dff", size = 106633 }
582 | wheels = [
583 | { url = "https://files.pythonhosted.org/packages/df/c5/e7a0b0f5ed69f94c8ab7379c599e6036886bffcde609969a5325f47f1332/typing_extensions-4.13.1-py3-none-any.whl", hash = "sha256:4b6cf02909eb5495cfbc3f6e8fd49217e6cc7944e145cdda8caa3734777f9e69", size = 45739 },
584 | ]
585 |
586 | [[package]]
587 | name = "typing-inspection"
588 | version = "0.4.0"
589 | source = { registry = "https://pypi.org/simple" }
590 | dependencies = [
591 | { name = "typing-extensions" },
592 | ]
593 | sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222 }
594 | wheels = [
595 | { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 },
596 | ]
597 |
598 | [[package]]
599 | name = "uvicorn"
600 | version = "0.34.0"
601 | source = { registry = "https://pypi.org/simple" }
602 | dependencies = [
603 | { name = "click" },
604 | { name = "h11" },
605 | ]
606 | sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 }
607 | wheels = [
608 | { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 },
609 | ]
610 |
611 | [package.optional-dependencies]
612 | standard = [
613 | { name = "colorama", marker = "sys_platform == 'win32'" },
614 | { name = "httptools" },
615 | { name = "python-dotenv" },
616 | { name = "pyyaml" },
617 | { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" },
618 | { name = "watchfiles" },
619 | { name = "websockets" },
620 | ]
621 |
622 | [[package]]
623 | name = "uvloop"
624 | version = "0.21.0"
625 | source = { registry = "https://pypi.org/simple" }
626 | sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741 }
627 | wheels = [
628 | { url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123 },
629 | { url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325 },
630 | { url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806 },
631 | { url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", size = 4701068 },
632 | { url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", size = 4454428 },
633 | { url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018 },
634 | ]
635 |
636 | [[package]]
637 | name = "virtualenv"
638 | version = "20.30.0"
639 | source = { registry = "https://pypi.org/simple" }
640 | dependencies = [
641 | { name = "distlib" },
642 | { name = "filelock" },
643 | { name = "platformdirs" },
644 | ]
645 | sdist = { url = "https://files.pythonhosted.org/packages/38/e0/633e369b91bbc664df47dcb5454b6c7cf441e8f5b9d0c250ce9f0546401e/virtualenv-20.30.0.tar.gz", hash = "sha256:800863162bcaa5450a6e4d721049730e7f2dae07720e0902b0e4040bd6f9ada8", size = 4346945 }
646 | wheels = [
647 | { url = "https://files.pythonhosted.org/packages/4c/ed/3cfeb48175f0671ec430ede81f628f9fb2b1084c9064ca67ebe8c0ed6a05/virtualenv-20.30.0-py3-none-any.whl", hash = "sha256:e34302959180fca3af42d1800df014b35019490b119eba981af27f2fa486e5d6", size = 4329461 },
648 | ]
649 |
650 | [[package]]
651 | name = "watchfiles"
652 | version = "1.0.4"
653 | source = { registry = "https://pypi.org/simple" }
654 | dependencies = [
655 | { name = "anyio" },
656 | ]
657 | sdist = { url = "https://files.pythonhosted.org/packages/f5/26/c705fc77d0a9ecdb9b66f1e2976d95b81df3cae518967431e7dbf9b5e219/watchfiles-1.0.4.tar.gz", hash = "sha256:6ba473efd11062d73e4f00c2b730255f9c1bdd73cd5f9fe5b5da8dbd4a717205", size = 94625 }
658 | wheels = [
659 | { url = "https://files.pythonhosted.org/packages/08/98/f03efabec64b5b1fa58c0daab25c68ef815b0f320e54adcacd0d6847c339/watchfiles-1.0.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:8012bd820c380c3d3db8435e8cf7592260257b378b649154a7948a663b5f84e9", size = 390954 },
660 | { url = "https://files.pythonhosted.org/packages/16/09/4dd49ba0a32a45813debe5fb3897955541351ee8142f586303b271a02b40/watchfiles-1.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa216f87594f951c17511efe5912808dfcc4befa464ab17c98d387830ce07b60", size = 381133 },
661 | { url = "https://files.pythonhosted.org/packages/76/59/5aa6fc93553cd8d8ee75c6247763d77c02631aed21551a97d94998bf1dae/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c9953cf85529c05b24705639ffa390f78c26449e15ec34d5339e8108c7c407", size = 449516 },
662 | { url = "https://files.pythonhosted.org/packages/4c/aa/df4b6fe14b6317290b91335b23c96b488d365d65549587434817e06895ea/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cf684aa9bba4cd95ecb62c822a56de54e3ae0598c1a7f2065d51e24637a3c5d", size = 454820 },
663 | { url = "https://files.pythonhosted.org/packages/5e/71/185f8672f1094ce48af33252c73e39b48be93b761273872d9312087245f6/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f44a39aee3cbb9b825285ff979ab887a25c5d336e5ec3574f1506a4671556a8d", size = 481550 },
664 | { url = "https://files.pythonhosted.org/packages/85/d7/50ebba2c426ef1a5cb17f02158222911a2e005d401caf5d911bfca58f4c4/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38320582736922be8c865d46520c043bff350956dfc9fbaee3b2df4e1740a4b", size = 518647 },
665 | { url = "https://files.pythonhosted.org/packages/f0/7a/4c009342e393c545d68987e8010b937f72f47937731225b2b29b7231428f/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39f4914548b818540ef21fd22447a63e7be6e24b43a70f7642d21f1e73371590", size = 497547 },
666 | { url = "https://files.pythonhosted.org/packages/0f/7c/1cf50b35412d5c72d63b2bf9a4fffee2e1549a245924960dd087eb6a6de4/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f12969a3765909cf5dc1e50b2436eb2c0e676a3c75773ab8cc3aa6175c16e902", size = 452179 },
667 | { url = "https://files.pythonhosted.org/packages/d6/a9/3db1410e1c1413735a9a472380e4f431ad9a9e81711cda2aaf02b7f62693/watchfiles-1.0.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:0986902677a1a5e6212d0c49b319aad9cc48da4bd967f86a11bde96ad9676ca1", size = 614125 },
668 | { url = "https://files.pythonhosted.org/packages/f2/e1/0025d365cf6248c4d1ee4c3d2e3d373bdd3f6aff78ba4298f97b4fad2740/watchfiles-1.0.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:308ac265c56f936636e3b0e3f59e059a40003c655228c131e1ad439957592303", size = 611911 },
669 | { url = "https://files.pythonhosted.org/packages/55/55/035838277d8c98fc8c917ac9beeb0cd6c59d675dc2421df5f9fcf44a0070/watchfiles-1.0.4-cp313-cp313-win32.whl", hash = "sha256:aee397456a29b492c20fda2d8961e1ffb266223625346ace14e4b6d861ba9c80", size = 271152 },
670 | { url = "https://files.pythonhosted.org/packages/f0/e5/96b8e55271685ddbadc50ce8bc53aa2dff278fb7ac4c2e473df890def2dc/watchfiles-1.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:d6097538b0ae5c1b88c3b55afa245a66793a8fec7ada6755322e465fb1a0e8cc", size = 285216 },
671 | ]
672 |
673 | [[package]]
674 | name = "websockets"
675 | version = "15.0.1"
676 | source = { registry = "https://pypi.org/simple" }
677 | sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016 }
678 | wheels = [
679 | { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440 },
680 | { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098 },
681 | { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329 },
682 | { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111 },
683 | { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054 },
684 | { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496 },
685 | { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829 },
686 | { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217 },
687 | { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195 },
688 | { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393 },
689 | { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837 },
690 | { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 },
691 | ]
692 |
--------------------------------------------------------------------------------
/version.txt:
--------------------------------------------------------------------------------
1 | 2.0.0
2 |
--------------------------------------------------------------------------------