├── .editorconfig
├── .github
└── workflows
│ ├── main.yml
│ └── release.yml
├── .gitignore
├── .pre-commit-config.yaml
├── LICENSE
├── README.md
├── example.svg
├── poetry.lock
├── pyproject.toml
├── qrplatba
├── __init__.py
├── spayd.py
└── svg.py
└── tests
└── test_svg.py
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | indent_style = space
7 | indent_size = 4
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 | max_line_length = 120
11 |
12 | [{*.yml,*.yaml}]
13 | indent_size = 2
14 |
15 | [Makefile]
16 | indent_style = tab
17 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on:
4 | push:
5 | paths-ignore:
6 | - 'README.md'
7 | - 'LICENSE'
8 | - 'example.svg'
9 | branches:
10 | - master
11 | pull_request:
12 | paths-ignore:
13 | - 'README.md'
14 | - 'LICENSE'
15 | - 'example.svg'
16 | branches:
17 | - '**'
18 |
19 | concurrency:
20 | group: tests-${{ github.head_ref || github.ref }}
21 | cancel-in-progress: ${{ github.event_name == 'pull_request' }}
22 |
23 | jobs:
24 | tests:
25 | name: "pytest: Python ${{ matrix.python-version }}"
26 | runs-on: ubuntu-latest
27 | strategy:
28 | matrix:
29 | python-version: [ "3.8", "3.9", "3.10", "3.11" ]
30 | defaults:
31 | run:
32 | shell: bash
33 | steps:
34 | - uses: actions/checkout@v3
35 |
36 | - name: Set up Python ${{ matrix.python-version }}
37 | uses: actions/setup-python@v4
38 | with:
39 | python-version: ${{ matrix.python-version }}
40 |
41 | - name: Get full Python version
42 | id: full-python-version
43 | run: echo version=$(python -c "import sys; print('-'.join(str(v) for v in sys.version_info))") >> $GITHUB_OUTPUT
44 |
45 | - name: Bootstrap poetry
46 | run: |
47 | curl -sSL https://install.python-poetry.org | python - -y
48 |
49 | - name: Update PATH
50 | run: echo "$HOME/.local/bin" >> $GITHUB_PATH
51 |
52 | - name: Configure poetry
53 | run: poetry config virtualenvs.in-project true
54 |
55 | - name: Set up cache
56 | uses: actions/cache@v3
57 | id: cache
58 | with:
59 | path: .venv
60 | key: venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('**/poetry.lock') }}
61 |
62 | - name: Ensure cache is healthy
63 | if: steps.cache.outputs.cache-hit == 'true'
64 | run: |
65 | # `timeout` is not available on macOS, so we define a custom function.
66 | [ "$(command -v timeout)" ] || function timeout() { perl -e 'alarm shift; exec @ARGV' "$@"; }
67 | # Using `timeout` is a safeguard against the Poetry command hanging for some reason.
68 | timeout 10s poetry run pip --version || rm -rf .venv
69 | - name: Check lock file
70 | run: poetry lock --check
71 |
72 | - name: Install dependencies
73 | run: poetry install --with test
74 |
75 | - name: Run pytest
76 | run: poetry run pytest -v
77 |
78 | - name: Run ruff
79 | run: poetry run ruff .
80 |
81 | - name: Run black
82 | run: poetry run black --check .
83 |
84 | - name: Check for clean working tree
85 | run: |
86 | git diff --exit-code --stat HEAD
87 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - '*.*.*'
7 |
8 | jobs:
9 | release:
10 | name: Release
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Checkout code
14 | uses: actions/checkout@v3
15 |
16 | - name: Set up Python 3.11
17 | uses: actions/setup-python@v4
18 | with:
19 | python-version: "3.11"
20 |
21 | - name: Install Poetry
22 | run: |
23 | curl -sSL https://install.python-poetry.org | python - -y
24 | - name: Update PATH
25 | run: echo "$HOME/.local/bin" >> $GITHUB_PATH
26 |
27 | - name: Build project for distribution
28 | run: poetry build
29 |
30 | - name: Create Release
31 | uses: ncipollo/release-action@v1
32 | with:
33 | artifacts: "dist/*"
34 | token: ${{ secrets.GITHUB_TOKEN }}
35 | draft: false
36 |
37 | - name: Publish to PyPI
38 | env:
39 | POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }}
40 | run: poetry publish
41 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__/
2 | *.pyc
3 | dist/
4 | .idea/
5 | qrplatba.egg-info/
6 | .*_cache/
7 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/psf/black
3 | rev: 23.3.0
4 | hooks:
5 | - id: black
6 | language_version: python3.11
7 | - repo: https://github.com/charliermarsh/ruff-pre-commit
8 | rev: 'v0.0.260'
9 | hooks:
10 | - id: ruff
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Viktor Stískala
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # python-qrplatba
2 |
3 | [](https://pypi.org/project/qrplatba/#description)
4 |
5 | Python library for generating QR codes for QR platba.
6 |
7 | 
8 |
9 | See http://qr-platba.cz/pro-vyvojare/ for more information about the specification (available only in czech).
10 |
11 | ```python
12 | from qrplatba import QRPlatbaGenerator
13 | from datetime import datetime, timedelta
14 |
15 |
16 | due = datetime.now() + timedelta(days=14)
17 | generator = QRPlatbaGenerator('123456789/0123', 400.56, x_vs=2034456, message='text', due_date=due)
18 | img = generator.make_image()
19 | img.save('example.svg')
20 |
21 | # optional: custom box size and border
22 | img = generator.make_image(box_size=20, border=4)
23 |
24 | # optional: get SVG as a string.
25 | # Encoding has to be 'unicode', otherwise it will be encoded as bytes
26 | svg_data = img.to_string(encoding='unicode')
27 | ```
28 |
29 | ## Installation
30 |
31 | To install qrplatba, simply:
32 |
33 | ```bash
34 | $ pip install qrplatba
35 | ```
36 |
37 | ## Note on image file formats
38 |
39 | This module generates SVG file which is an XML-based vector image format. You can use various libraries and/or utilities to convert it to other vector or bitmap image formats. Below is an example how to use ``libRSVG`` to convert SVG images.
40 |
41 | ### libRSVG
42 |
43 | [`libRSVG`](https://wiki.gnome.org/action/show/Projects/LibRsvg?action=show) renders SVG files using cairo and supports many output image formats. It can also be used directly in console with ``rsvg-convert`` command.
44 |
45 | ```bash
46 | $ rsvg-convert -f pdf example.svg -o example.pdf
47 | ```
48 |
49 | ## SPAYD format
50 |
51 | QR Platba uses SPAYD format (`application/x-shortpaymentdescriptor`) for encoding information related to bank transfer. In addition to generating QR codes, this library can also generate just the encoded string using the following code:
52 |
53 | ```python
54 | generator = QRPlatbaGenerator('123456789/0123', 400.56, x_vs=2034456, message='text', due_date=due)
55 | spayd = generator.get_text()
56 | ```
57 |
58 | ## License
59 |
60 | This software is licensed under [MIT license](https://opensource.org/license/mit/) since version `1.0.0`.
61 |
62 | ## Changelog
63 |
64 | ### `1.1.1` (24 April 2023)
65 | - Added compatibility with `lxml` library. Fixes `TypeError` when using this library while `lxml` is installed in the same virtualenv.
66 |
67 | ### `1.1.0` (5 April 2023)
68 |
69 | - Dropped support for Python 3.7
70 | - Added pre-commit, black and ruff for code formatting
71 |
72 | ### `1.0.0` (4 April 2023)
73 |
74 | **Warning:** While the API is mostly backwards compatible, the look and size of the generated QR codes has changed.
75 |
76 | - Updated requirements to support the latest `qrcode` version
77 | - Added support for custom output sizes using `box_size` and `border` parameters
78 | - Changed legacy setuptools to [poetry](https://python-poetry.org/)
79 | - Dropped support for Python `2.x` and `<3.7`
80 | - Changed license to MIT
81 | - Added unit tests
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/example.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/poetry.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand.
2 |
3 | [[package]]
4 | name = "black"
5 | version = "23.3.0"
6 | description = "The uncompromising code formatter."
7 | category = "dev"
8 | optional = false
9 | python-versions = ">=3.7"
10 | files = [
11 | {file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"},
12 | {file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"},
13 | {file = "black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"},
14 | {file = "black-23.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c"},
15 | {file = "black-23.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c"},
16 | {file = "black-23.3.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6"},
17 | {file = "black-23.3.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b"},
18 | {file = "black-23.3.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d"},
19 | {file = "black-23.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70"},
20 | {file = "black-23.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326"},
21 | {file = "black-23.3.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b"},
22 | {file = "black-23.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2"},
23 | {file = "black-23.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925"},
24 | {file = "black-23.3.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27"},
25 | {file = "black-23.3.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331"},
26 | {file = "black-23.3.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5"},
27 | {file = "black-23.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961"},
28 | {file = "black-23.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8"},
29 | {file = "black-23.3.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30"},
30 | {file = "black-23.3.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3"},
31 | {file = "black-23.3.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266"},
32 | {file = "black-23.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab"},
33 | {file = "black-23.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb"},
34 | {file = "black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"},
35 | {file = "black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"},
36 | ]
37 |
38 | [package.dependencies]
39 | click = ">=8.0.0"
40 | mypy-extensions = ">=0.4.3"
41 | packaging = ">=22.0"
42 | pathspec = ">=0.9.0"
43 | platformdirs = ">=2"
44 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
45 | typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}
46 |
47 | [package.extras]
48 | colorama = ["colorama (>=0.4.3)"]
49 | d = ["aiohttp (>=3.7.4)"]
50 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
51 | uvloop = ["uvloop (>=0.15.2)"]
52 |
53 | [[package]]
54 | name = "cfgv"
55 | version = "3.3.1"
56 | description = "Validate configuration and produce human readable error messages."
57 | category = "dev"
58 | optional = false
59 | python-versions = ">=3.6.1"
60 | files = [
61 | {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"},
62 | {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
63 | ]
64 |
65 | [[package]]
66 | name = "click"
67 | version = "8.1.3"
68 | description = "Composable command line interface toolkit"
69 | category = "dev"
70 | optional = false
71 | python-versions = ">=3.7"
72 | files = [
73 | {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
74 | {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
75 | ]
76 |
77 | [package.dependencies]
78 | colorama = {version = "*", markers = "platform_system == \"Windows\""}
79 |
80 | [[package]]
81 | name = "colorama"
82 | version = "0.4.6"
83 | description = "Cross-platform colored terminal text."
84 | category = "main"
85 | optional = false
86 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
87 | files = [
88 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
89 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
90 | ]
91 |
92 | [[package]]
93 | name = "distlib"
94 | version = "0.3.6"
95 | description = "Distribution utilities"
96 | category = "dev"
97 | optional = false
98 | python-versions = "*"
99 | files = [
100 | {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"},
101 | {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"},
102 | ]
103 |
104 | [[package]]
105 | name = "exceptiongroup"
106 | version = "1.1.1"
107 | description = "Backport of PEP 654 (exception groups)"
108 | category = "dev"
109 | optional = false
110 | python-versions = ">=3.7"
111 | files = [
112 | {file = "exceptiongroup-1.1.1-py3-none-any.whl", hash = "sha256:232c37c63e4f682982c8b6459f33a8981039e5fb8756b2074364e5055c498c9e"},
113 | {file = "exceptiongroup-1.1.1.tar.gz", hash = "sha256:d484c3090ba2889ae2928419117447a14daf3c1231d5e30d0aae34f354f01785"},
114 | ]
115 |
116 | [package.extras]
117 | test = ["pytest (>=6)"]
118 |
119 | [[package]]
120 | name = "filelock"
121 | version = "3.12.0"
122 | description = "A platform independent file lock."
123 | category = "dev"
124 | optional = false
125 | python-versions = ">=3.7"
126 | files = [
127 | {file = "filelock-3.12.0-py3-none-any.whl", hash = "sha256:ad98852315c2ab702aeb628412cbf7e95b7ce8c3bf9565670b4eaecf1db370a9"},
128 | {file = "filelock-3.12.0.tar.gz", hash = "sha256:fc03ae43288c013d2ea83c8597001b1129db351aad9c57fe2409327916b8e718"},
129 | ]
130 |
131 | [package.extras]
132 | docs = ["furo (>=2023.3.27)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"]
133 | testing = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"]
134 |
135 | [[package]]
136 | name = "identify"
137 | version = "2.5.22"
138 | description = "File identification library for Python"
139 | category = "dev"
140 | optional = false
141 | python-versions = ">=3.7"
142 | files = [
143 | {file = "identify-2.5.22-py2.py3-none-any.whl", hash = "sha256:f0faad595a4687053669c112004178149f6c326db71ee999ae4636685753ad2f"},
144 | {file = "identify-2.5.22.tar.gz", hash = "sha256:f7a93d6cf98e29bd07663c60728e7a4057615068d7a639d132dc883b2d54d31e"},
145 | ]
146 |
147 | [package.extras]
148 | license = ["ukkonen"]
149 |
150 | [[package]]
151 | name = "iniconfig"
152 | version = "2.0.0"
153 | description = "brain-dead simple config-ini parsing"
154 | category = "dev"
155 | optional = false
156 | python-versions = ">=3.7"
157 | files = [
158 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
159 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
160 | ]
161 |
162 | [[package]]
163 | name = "mypy-extensions"
164 | version = "1.0.0"
165 | description = "Type system extensions for programs checked with the mypy type checker."
166 | category = "dev"
167 | optional = false
168 | python-versions = ">=3.5"
169 | files = [
170 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
171 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
172 | ]
173 |
174 | [[package]]
175 | name = "nodeenv"
176 | version = "1.7.0"
177 | description = "Node.js virtual environment builder"
178 | category = "dev"
179 | optional = false
180 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
181 | files = [
182 | {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"},
183 | {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"},
184 | ]
185 |
186 | [package.dependencies]
187 | setuptools = "*"
188 |
189 | [[package]]
190 | name = "packaging"
191 | version = "23.1"
192 | description = "Core utilities for Python packages"
193 | category = "dev"
194 | optional = false
195 | python-versions = ">=3.7"
196 | files = [
197 | {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"},
198 | {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"},
199 | ]
200 |
201 | [[package]]
202 | name = "pathspec"
203 | version = "0.11.1"
204 | description = "Utility library for gitignore style pattern matching of file paths."
205 | category = "dev"
206 | optional = false
207 | python-versions = ">=3.7"
208 | files = [
209 | {file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"},
210 | {file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"},
211 | ]
212 |
213 | [[package]]
214 | name = "platformdirs"
215 | version = "3.2.0"
216 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
217 | category = "dev"
218 | optional = false
219 | python-versions = ">=3.7"
220 | files = [
221 | {file = "platformdirs-3.2.0-py3-none-any.whl", hash = "sha256:ebe11c0d7a805086e99506aa331612429a72ca7cd52a1f0d277dc4adc20cb10e"},
222 | {file = "platformdirs-3.2.0.tar.gz", hash = "sha256:d5b638ca397f25f979350ff789db335903d7ea010ab28903f57b27e1b16c2b08"},
223 | ]
224 |
225 | [package.extras]
226 | docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"]
227 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"]
228 |
229 | [[package]]
230 | name = "pluggy"
231 | version = "1.0.0"
232 | description = "plugin and hook calling mechanisms for python"
233 | category = "dev"
234 | optional = false
235 | python-versions = ">=3.6"
236 | files = [
237 | {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
238 | {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
239 | ]
240 |
241 | [package.extras]
242 | dev = ["pre-commit", "tox"]
243 | testing = ["pytest", "pytest-benchmark"]
244 |
245 | [[package]]
246 | name = "pre-commit"
247 | version = "3.2.2"
248 | description = "A framework for managing and maintaining multi-language pre-commit hooks."
249 | category = "dev"
250 | optional = false
251 | python-versions = ">=3.8"
252 | files = [
253 | {file = "pre_commit-3.2.2-py2.py3-none-any.whl", hash = "sha256:0b4210aea813fe81144e87c5a291f09ea66f199f367fa1df41b55e1d26e1e2b4"},
254 | {file = "pre_commit-3.2.2.tar.gz", hash = "sha256:5b808fcbda4afbccf6d6633a56663fed35b6c2bc08096fd3d47ce197ac351d9d"},
255 | ]
256 |
257 | [package.dependencies]
258 | cfgv = ">=2.0.0"
259 | identify = ">=1.0.0"
260 | nodeenv = ">=0.11.1"
261 | pyyaml = ">=5.1"
262 | virtualenv = ">=20.10.0"
263 |
264 | [[package]]
265 | name = "pypng"
266 | version = "0.20220715.0"
267 | description = "Pure Python library for saving and loading PNG images"
268 | category = "main"
269 | optional = false
270 | python-versions = "*"
271 | files = [
272 | {file = "pypng-0.20220715.0-py3-none-any.whl", hash = "sha256:4a43e969b8f5aaafb2a415536c1a8ec7e341cd6a3f957fd5b5f32a4cfeed902c"},
273 | {file = "pypng-0.20220715.0.tar.gz", hash = "sha256:739c433ba96f078315de54c0db975aee537cbc3e1d0ae4ed9aab0ca1e427e2c1"},
274 | ]
275 |
276 | [[package]]
277 | name = "pytest"
278 | version = "7.3.1"
279 | description = "pytest: simple powerful testing with Python"
280 | category = "dev"
281 | optional = false
282 | python-versions = ">=3.7"
283 | files = [
284 | {file = "pytest-7.3.1-py3-none-any.whl", hash = "sha256:3799fa815351fea3a5e96ac7e503a96fa51cc9942c3753cda7651b93c1cfa362"},
285 | {file = "pytest-7.3.1.tar.gz", hash = "sha256:434afafd78b1d78ed0addf160ad2b77a30d35d4bdf8af234fe621919d9ed15e3"},
286 | ]
287 |
288 | [package.dependencies]
289 | colorama = {version = "*", markers = "sys_platform == \"win32\""}
290 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
291 | iniconfig = "*"
292 | packaging = "*"
293 | pluggy = ">=0.12,<2.0"
294 | tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
295 |
296 | [package.extras]
297 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
298 |
299 | [[package]]
300 | name = "pytest-github-actions-annotate-failures"
301 | version = "0.1.8"
302 | description = "pytest plugin to annotate failed tests with a workflow command for GitHub Actions"
303 | category = "dev"
304 | optional = false
305 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*"
306 | files = [
307 | {file = "pytest-github-actions-annotate-failures-0.1.8.tar.gz", hash = "sha256:2d6e6cb5f8d0aae4a27a20cc4e20fabd3199a121c57f44bc48fe28e372e0be23"},
308 | {file = "pytest_github_actions_annotate_failures-0.1.8-py2.py3-none-any.whl", hash = "sha256:6a882ff21672fa79deae8d917eb965a6bde2b25191e7632e1adfc23ffac008ab"},
309 | ]
310 |
311 | [package.dependencies]
312 | pytest = ">=4.0.0"
313 |
314 | [[package]]
315 | name = "pyyaml"
316 | version = "6.0"
317 | description = "YAML parser and emitter for Python"
318 | category = "dev"
319 | optional = false
320 | python-versions = ">=3.6"
321 | files = [
322 | {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
323 | {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"},
324 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"},
325 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"},
326 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"},
327 | {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"},
328 | {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"},
329 | {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"},
330 | {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"},
331 | {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"},
332 | {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"},
333 | {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"},
334 | {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"},
335 | {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"},
336 | {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"},
337 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"},
338 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"},
339 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"},
340 | {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"},
341 | {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"},
342 | {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"},
343 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"},
344 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"},
345 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"},
346 | {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"},
347 | {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"},
348 | {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"},
349 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"},
350 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"},
351 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"},
352 | {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"},
353 | {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"},
354 | {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"},
355 | {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"},
356 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"},
357 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"},
358 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"},
359 | {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"},
360 | {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"},
361 | {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
362 | ]
363 |
364 | [[package]]
365 | name = "qrcode"
366 | version = "7.4.2"
367 | description = "QR Code image generator"
368 | category = "main"
369 | optional = false
370 | python-versions = ">=3.7"
371 | files = [
372 | {file = "qrcode-7.4.2-py3-none-any.whl", hash = "sha256:581dca7a029bcb2deef5d01068e39093e80ef00b4a61098a2182eac59d01643a"},
373 | {file = "qrcode-7.4.2.tar.gz", hash = "sha256:9dd969454827e127dbd93696b20747239e6d540e082937c90f14ac95b30f5845"},
374 | ]
375 |
376 | [package.dependencies]
377 | colorama = {version = "*", markers = "platform_system == \"Windows\""}
378 | pypng = "*"
379 | typing-extensions = "*"
380 |
381 | [package.extras]
382 | all = ["pillow (>=9.1.0)", "pytest", "pytest-cov", "tox", "zest.releaser[recommended]"]
383 | dev = ["pytest", "pytest-cov", "tox"]
384 | maintainer = ["zest.releaser[recommended]"]
385 | pil = ["pillow (>=9.1.0)"]
386 | test = ["coverage", "pytest"]
387 |
388 | [[package]]
389 | name = "ruff"
390 | version = "0.0.261"
391 | description = "An extremely fast Python linter, written in Rust."
392 | category = "dev"
393 | optional = false
394 | python-versions = ">=3.7"
395 | files = [
396 | {file = "ruff-0.0.261-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:6624a966c4a21110cee6780333e2216522a831364896f3d98f13120936eff40a"},
397 | {file = "ruff-0.0.261-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:2dba68a9e558ab33e6dd5d280af798a2d9d3c80c913ad9c8b8e97d7b287f1cc9"},
398 | {file = "ruff-0.0.261-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dbd0cee5a81b0785dc0feeb2640c1e31abe93f0d77c5233507ac59731a626f1"},
399 | {file = "ruff-0.0.261-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:581e64fa1518df495ca890a605ee65065101a86db56b6858f848bade69fc6489"},
400 | {file = "ruff-0.0.261-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc970f6ece0b4950e419f0252895ee42e9e8e5689c6494d18f5dc2c6ebb7f798"},
401 | {file = "ruff-0.0.261-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8fa98e747e0fe185d65a40b0ea13f55c492f3b5f9a032a1097e82edaddb9e52e"},
402 | {file = "ruff-0.0.261-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f268d52a71bf410aa45c232870c17049df322a7d20e871cfe622c9fc784aab7b"},
403 | {file = "ruff-0.0.261-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1293acc64eba16a11109678dc4743df08c207ed2edbeaf38b3e10eb2597321b"},
404 | {file = "ruff-0.0.261-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d95596e2f4cafead19a6d1ec0b86f8fda45ba66fe934de3956d71146a87959b3"},
405 | {file = "ruff-0.0.261-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4bcec45abdf65c1328a269cf6cc193f7ff85b777fa2865c64cf2c96b80148a2c"},
406 | {file = "ruff-0.0.261-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6c5f397ec0af42a434ad4b6f86565027406c5d0d0ebeea0d5b3f90c4bf55bc82"},
407 | {file = "ruff-0.0.261-py3-none-musllinux_1_2_i686.whl", hash = "sha256:39abd02342cec0c131b2ddcaace08b2eae9700cab3ca7dba64ae5fd4f4881bd0"},
408 | {file = "ruff-0.0.261-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:aaa4f52a6e513f8daa450dac4859e80390d947052f592f0d8e796baab24df2fc"},
409 | {file = "ruff-0.0.261-py3-none-win32.whl", hash = "sha256:daff64b4e86e42ce69e6367d63aab9562fc213cd4db0e146859df8abc283dba0"},
410 | {file = "ruff-0.0.261-py3-none-win_amd64.whl", hash = "sha256:0fbc689c23609edda36169c8708bb91bab111d8f44cb4a88330541757770ab30"},
411 | {file = "ruff-0.0.261-py3-none-win_arm64.whl", hash = "sha256:d2eddc60ae75fc87f8bb8fd6e8d5339cf884cd6de81e82a50287424309c187ba"},
412 | {file = "ruff-0.0.261.tar.gz", hash = "sha256:c1c715b0d1e18f9c509d7c411ca61da3543a4aa459325b1b1e52b8301d65c6d2"},
413 | ]
414 |
415 | [[package]]
416 | name = "setuptools"
417 | version = "67.7.1"
418 | description = "Easily download, build, install, upgrade, and uninstall Python packages"
419 | category = "dev"
420 | optional = false
421 | python-versions = ">=3.7"
422 | files = [
423 | {file = "setuptools-67.7.1-py3-none-any.whl", hash = "sha256:6f0839fbdb7e3cfef1fc38d7954f5c1c26bf4eebb155a55c9bf8faf997b9fb67"},
424 | {file = "setuptools-67.7.1.tar.gz", hash = "sha256:bb16732e8eb928922eabaa022f881ae2b7cdcfaf9993ef1f5e841a96d32b8e0c"},
425 | ]
426 |
427 | [package.extras]
428 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
429 | testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
430 | testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
431 |
432 | [[package]]
433 | name = "tomli"
434 | version = "2.0.1"
435 | description = "A lil' TOML parser"
436 | category = "dev"
437 | optional = false
438 | python-versions = ">=3.7"
439 | files = [
440 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
441 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
442 | ]
443 |
444 | [[package]]
445 | name = "typing-extensions"
446 | version = "4.5.0"
447 | description = "Backported and Experimental Type Hints for Python 3.7+"
448 | category = "main"
449 | optional = false
450 | python-versions = ">=3.7"
451 | files = [
452 | {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"},
453 | {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"},
454 | ]
455 |
456 | [[package]]
457 | name = "virtualenv"
458 | version = "20.22.0"
459 | description = "Virtual Python Environment builder"
460 | category = "dev"
461 | optional = false
462 | python-versions = ">=3.7"
463 | files = [
464 | {file = "virtualenv-20.22.0-py3-none-any.whl", hash = "sha256:48fd3b907b5149c5aab7c23d9790bea4cac6bc6b150af8635febc4cfeab1275a"},
465 | {file = "virtualenv-20.22.0.tar.gz", hash = "sha256:278753c47aaef1a0f14e6db8a4c5e1e040e90aea654d0fc1dc7e0d8a42616cc3"},
466 | ]
467 |
468 | [package.dependencies]
469 | distlib = ">=0.3.6,<1"
470 | filelock = ">=3.11,<4"
471 | platformdirs = ">=3.2,<4"
472 |
473 | [package.extras]
474 | docs = ["furo (>=2023.3.27)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"]
475 | test = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.3.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)"]
476 |
477 | [metadata]
478 | lock-version = "2.0"
479 | python-versions = "^3.8"
480 | content-hash = "8f83be87a044a2e37239ae98d848534271abd12e805eb46c1e1235fecf071bbf"
481 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.poetry]
2 | name = "qrplatba"
3 | version = "1.1.1"
4 | description = "QR platba SVG QR code and SPAYD string generator."
5 | authors = ["Viktor Stískala "]
6 | repository = "https://github.com/ViktorStiskala/python-qrplatba"
7 | classifiers=[
8 | 'Intended Audience :: Developers',
9 | 'Operating System :: OS Independent',
10 | 'Programming Language :: Python',
11 | 'Programming Language :: Python :: 3.8',
12 | 'Programming Language :: Python :: 3.9',
13 | 'Programming Language :: Python :: 3.10',
14 | 'Programming Language :: Python :: 3.11',
15 | 'Topic :: Software Development :: Libraries',
16 | 'Topic :: Software Development :: Libraries :: Python Modules',
17 | ]
18 | license = "MIT"
19 | readme = "README.md"
20 |
21 | [tool.poetry.dependencies]
22 | python = "^3.8"
23 | qrcode = "^7.4"
24 |
25 | [tool.poetry.group.test]
26 | optional = true
27 | [tool.poetry.group.test.dependencies]
28 | pytest = "^7.2"
29 | pytest-github-actions-annotate-failures = "^0.1.8"
30 | ruff = "^0.0.261"
31 | black = "^23.3.0"
32 |
33 | [tool.poetry.group.dev.dependencies]
34 | pre-commit = "^3.2.2"
35 |
36 | [tool.ruff]
37 | line-length = 120
38 |
39 | [tool.black]
40 | line-length = 120
41 |
42 | [build-system]
43 | requires = ["poetry-core>=1.0.0"]
44 | build-backend = "poetry.core.masonry.api"
45 |
--------------------------------------------------------------------------------
/qrplatba/__init__.py:
--------------------------------------------------------------------------------
1 | from .spayd import QRPlatbaGenerator
2 |
3 | __all__ = ["QRPlatbaGenerator"]
4 |
--------------------------------------------------------------------------------
/qrplatba/spayd.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime, date
2 | import re
3 | import qrcode
4 | from qrplatba.svg import QRPlatbaSVGImage
5 |
6 |
7 | class QRPlatbaGenerator:
8 | RE_ACCOUNT = re.compile(r"((?P\d+(?=-))-)?(?P\d+)/(?P\d{4})")
9 |
10 | def __init__(
11 | self,
12 | account,
13 | amount=None,
14 | currency=None,
15 | x_vs=None,
16 | x_ss=None,
17 | x_ks=None,
18 | alternate_accounts=None,
19 | recipient_name=None,
20 | due_date=None,
21 | payment_type=None,
22 | message=None,
23 | notification_type=None,
24 | notification_address=None,
25 | x_per=None,
26 | x_id=None,
27 | x_url=None,
28 | reference=None,
29 | ):
30 | """
31 | http://qr-platba.cz/pro-vyvojare/specifikace-formatu/
32 |
33 | :param account: ACC account number, can be specified either as IBAN or in CZ format 12-123456789/0300
34 | :param amount: AM payment amount
35 | :param currency: CC currency (3 digits)
36 | :param x_vs: X-VS
37 | :param x_ss: X-SS
38 | :param x_ks: X-KS
39 | :param alternate_accounts: ALT-ACC
40 | :param recipient_name: RN recipient name
41 | :param due_date: DT due date, IS0 8601
42 | :param payment_type: PT max 3 digits
43 | :param message: MSG message for recipient, max 60 chars
44 | :param notification_type: NT P for phone or E for email
45 | :param notification_address: NTA
46 | :param x_per: X-PER number of days to repeat payment if unsuccessful
47 | :param x_id: X-ID
48 | :param x_url: X-URL
49 | :param reference: RF recipient reference number. Max 16 digits. integer.
50 | """
51 | self.account = account
52 | self.amount = amount
53 | self.currency = currency
54 | self.x_vs = x_vs
55 | self.x_ss = x_ss
56 | self.x_ks = x_ks
57 | self.alternate_accounts = alternate_accounts
58 | self.recipient_name = recipient_name
59 | self.due_date = due_date
60 | self.payment_type = payment_type
61 | self.message = message
62 | self.notification_type = notification_type
63 | self.notification_address = notification_address
64 | self.x_per = x_per
65 | self.x_id = x_id
66 | self.x_url = x_url
67 | self.reference = reference
68 |
69 | def _convert_to_iban(self, account):
70 | """
71 | Convert czech account number to IBAN
72 | """
73 | acc = self.RE_ACCOUNT.match(account)
74 | iban = "CZ00{b}{ba:0>6}{a:0>10}".format(
75 | ba=acc.group("ba") or 0,
76 | a=acc.group("a"),
77 | b=acc.group("b"),
78 | )
79 |
80 | # convert IBAN letters into numbers
81 | crc = re.sub(r"[A-Z]", lambda m: str(ord(m.group(0)) - 55), iban[4:] + iban[:4])
82 |
83 | # compute control digits
84 | digits = "{:0>2}".format(98 - int(crc) % 97)
85 |
86 | return iban[:2] + digits + iban[4:]
87 |
88 | @property
89 | def _account(self):
90 | if self.account is not None:
91 | out = "ACC:{}*"
92 |
93 | if self.RE_ACCOUNT.match(self.account):
94 | return out.format(self._convert_to_iban(self.account))
95 | return out.format(self.account)
96 | return ""
97 |
98 | @property
99 | def _alternate_accounts(self):
100 | if self.alternate_accounts is not None:
101 | formatted = []
102 | for account in self.alternate_accounts:
103 | if self.RE_ACCOUNT.match(account):
104 | formatted.append(self._convert_to_iban(account))
105 | else:
106 | formatted.append(account)
107 | return "ALT-ACC:{}*".format(",".join(formatted))
108 | return ""
109 |
110 | @property
111 | def _amount(self):
112 | if self.amount is not None:
113 | return "AM:{:.2f}*".format(self.amount)
114 | return ""
115 |
116 | @property
117 | def _due_date(self):
118 | if self.due_date is not None:
119 | str_part = "DT:{}*"
120 | if isinstance(self.due_date, datetime):
121 | return str_part.format(self.due_date.date().isoformat()).replace("-", "")
122 | if isinstance(self.due_date, date):
123 | return str_part.format(self.due_date.isoformat()).replace("-", "")
124 | return str_part.format(self.due_date)
125 | return ""
126 |
127 | def _format_item_string(self, item, name):
128 | if item:
129 | return "{name}:{value}*".format(name=name, value=item)
130 | return ""
131 |
132 | def get_text(self):
133 | return "SPD*1.0*{ACC}{ALTACC}{AM}{CC}{RF}{RN}{DT}{PT}{MSG}{NT}{NTA}{XPER}{XVS}{XSS}{XKS}{XID}{XURL}".format(
134 | ACC=self._account,
135 | ALTACC=self._alternate_accounts,
136 | AM=self._amount,
137 | CC=self._format_item_string(self.currency, "CC"),
138 | RF=self._format_item_string(self.reference, "RF"),
139 | RN=self._format_item_string(self.recipient_name, "RN"),
140 | DT=self._due_date,
141 | PT=self._format_item_string(self.payment_type, "PT"),
142 | MSG=self._format_item_string(self.message, "MSG"),
143 | NT=self._format_item_string(self.notification_type, "NT"),
144 | NTA=self._format_item_string(self.notification_address, "NTA"),
145 | XPER=self._format_item_string(self.x_per, "X-PER"),
146 | XVS=self._format_item_string(self.x_vs, "X-VS"),
147 | XSS=self._format_item_string(self.x_ss, "X-SS"),
148 | XKS=self._format_item_string(self.x_ks, "X-KS"),
149 | XID=self._format_item_string(self.x_id, "X-ID"),
150 | XURL=self._format_item_string(self.x_url, "X-URL"),
151 | ).rstrip("*")
152 |
153 | def make_image(self, border=2, box_size=12, error_correction=qrcode.constants.ERROR_CORRECT_M):
154 | qr = qrcode.QRCode(
155 | version=None,
156 | error_correction=error_correction,
157 | image_factory=QRPlatbaSVGImage,
158 | border=border,
159 | box_size=box_size,
160 | )
161 | qr.add_data(self.get_text())
162 | qr.make(fit=True)
163 |
164 | return qr.make_image()
165 |
--------------------------------------------------------------------------------
/qrplatba/svg.py:
--------------------------------------------------------------------------------
1 | from decimal import Decimal
2 | from typing import NamedTuple
3 |
4 | from qrcode.image import svg
5 | from qrcode.compat.etree import ET
6 |
7 |
8 | class ScaledSizes(NamedTuple):
9 | inside_border: Decimal
10 | outside_border: Decimal
11 | width: Decimal
12 | line_size: Decimal
13 | ratio: Decimal
14 |
15 |
16 | class QRPlatbaSVGImage(svg.SvgPathImage):
17 | """
18 | QR Platba SVG image generator.
19 |
20 | Inner padding is created according to specification (http://qr-platba.cz/pro-vyvojare/specifikace-formatu/),
21 | text size is computed to achieve width of 16 QR points.
22 | """
23 |
24 | QR_TEXT_STYLE = "font-size:{size}px;font-weight:bold;fill:#000000;font-family:Arial;"
25 | FONT_SIZE = Decimal("3.5")
26 | FONT_HEIGHT = Decimal("8")
27 |
28 | LINE_SIZE = Decimal("0.25")
29 | INSIDE_BORDER = 4
30 |
31 | BOTTOM_LINE_SEGMENTS = (2, 22)
32 |
33 | def __init__(self, border, width, box_size, *args, **kwargs):
34 | self.outside_border = border
35 | border += self.INSIDE_BORDER + self.LINE_SIZE # outside border + inside border + line size
36 |
37 | super().__init__(border, width, box_size, *args, **kwargs)
38 |
39 | def _get_scaled_sizes(self):
40 | """Computes sizes of the QR code and QR text according to the scale ratio"""
41 | scale_ratio = self.units(self.box_size, text=False)
42 |
43 | def strip_zeros(value):
44 | return Decimal(str(value).rstrip("0").rstrip(".")) if "." in str(value) else value
45 |
46 | return ScaledSizes(
47 | inside_border=strip_zeros(self.INSIDE_BORDER * scale_ratio),
48 | outside_border=strip_zeros(self.outside_border * scale_ratio),
49 | width=strip_zeros(self.width * scale_ratio),
50 | line_size=strip_zeros(self.LINE_SIZE * scale_ratio),
51 | ratio=scale_ratio,
52 | )
53 |
54 | def make_border(self):
55 | """Creates black thin border around QR code"""
56 | scaled = self._get_scaled_sizes()
57 |
58 | def sizes(ob, ib, wd, ln): # size helper
59 | return ob * scaled.outside_border + ib * scaled.inside_border + wd * scaled.width + ln * scaled.line_size
60 |
61 | horizontal_line = "M{x0},{y0}h{length}v{width}h-{length}z"
62 | vertical_line = "M{x0},{y0}v{length}h{width}v-{length}z"
63 |
64 | def get_subpaths():
65 | # top line
66 | yield horizontal_line.format(
67 | x0=scaled.outside_border,
68 | y0=scaled.outside_border,
69 | length=sizes(0, 2, 1, 2),
70 | width=scaled.line_size,
71 | )
72 |
73 | b_first, b_second = self.BOTTOM_LINE_SEGMENTS
74 |
75 | # bottom line - first segment
76 | yield horizontal_line.format(
77 | x0=scaled.outside_border,
78 | y0=sizes(1, 2, 1, 1),
79 | length=b_first * scaled.ratio,
80 | width=scaled.line_size,
81 | )
82 |
83 | # bottom line - second segment
84 | yield horizontal_line.format(
85 | x0=scaled.outside_border + b_second * scaled.ratio,
86 | y0=sizes(1, 2, 1, 1),
87 | length=sizes(0, 2, 1, 2) - b_second * scaled.ratio,
88 | width=scaled.line_size,
89 | )
90 |
91 | # left line
92 | yield vertical_line.format(
93 | x0=scaled.outside_border,
94 | y0=scaled.outside_border + scaled.line_size,
95 | length=scaled.width + 2 * scaled.inside_border,
96 | width=scaled.line_size,
97 | )
98 |
99 | # right line
100 | yield vertical_line.format(
101 | x0=sizes(1, 2, 1, 1),
102 | y0=sizes(1, 0, 0, 1),
103 | length=sizes(0, 2, 1, 0),
104 | width=scaled.line_size,
105 | )
106 |
107 | subpaths = " ".join(get_subpaths())
108 | return ET.Element("path", d=subpaths, id="qrplatba-border", **self.QR_PATH_STYLE)
109 |
110 | def make_text(self):
111 | """Creates "QR platba" text element"""
112 | scaled = self._get_scaled_sizes()
113 | text_style = self.QR_TEXT_STYLE.format(size=(self.FONT_SIZE * scaled.ratio).quantize(Decimal("0.01")))
114 |
115 | x_pos = str(scaled.outside_border + scaled.line_size + 4 * scaled.ratio)
116 | y_pos = str(
117 | scaled.outside_border
118 | + scaled.line_size
119 | + 2 * scaled.inside_border
120 | + scaled.width
121 | + (self.FONT_HEIGHT / 4) * scaled.ratio
122 | )
123 |
124 | text_el = ET.Element("text", style=text_style, x=x_pos, y=y_pos, id="qrplatba-text")
125 | text_el.text = "QR platba"
126 |
127 | return text_el
128 |
129 | def _svg(self, viewBox=None, **kwargs):
130 | scaled = self._get_scaled_sizes()
131 | h_pixels = self.pixel_size + (self.FONT_HEIGHT * scaled.ratio)
132 |
133 | box = "0 0 {w} {h}".format(
134 | w=self.units(self.pixel_size, text=False),
135 | h=self.units(h_pixels, text=False),
136 | )
137 | svg_el = super()._svg(viewBox=box, **kwargs)
138 | svg_el.append(self.make_border())
139 | svg_el.append(self.make_text())
140 |
141 | # update size of the SVG element
142 | svg_el.attrib["height"] = str(self.units(h_pixels))
143 |
144 | return svg_el
145 |
--------------------------------------------------------------------------------
/tests/test_svg.py:
--------------------------------------------------------------------------------
1 | from decimal import Decimal
2 |
3 | from qrplatba import QRPlatbaGenerator
4 | from datetime import datetime, timedelta
5 | import xml.etree.ElementTree as ET
6 |
7 |
8 | class TestSVGImage:
9 | data = {
10 | "account": "123456789/0123",
11 | "amount": 400.56,
12 | "x_vs": 2034456,
13 | "message": "text",
14 | "due_date": datetime.now() + timedelta(days=14),
15 | }
16 |
17 | def make_image(self, **kwargs):
18 | generator = QRPlatbaGenerator(**self.data)
19 | img = generator.make_image(**kwargs)
20 |
21 | return img.to_string(encoding="unicode")
22 |
23 | def test_svg_content(self):
24 | svg_data = self.make_image()
25 |
26 | assert "http://www.w3.org/2000/svg" in svg_data
27 |
28 | root = ET.fromstring(svg_data)
29 | text = root.find(".//{http://www.w3.org/2000/svg}text")
30 | assert text is not None
31 | assert text.text == "QR platba"
32 |
33 | paths = root.findall(".//{http://www.w3.org/2000/svg}path")
34 | assert len(paths) == 2
35 |
36 | for path in paths:
37 | assert path.get("id") is not None
38 | assert path.get("id") in ("qr-path", "qrplatba-border")
39 |
40 | def test_svg_scaling(self):
41 | svg_data = self.make_image(box_size=40)
42 |
43 | root = ET.fromstring(svg_data)
44 |
45 | assert root is not None
46 | assert root.get("width") == "198mm"
47 | assert root.get("height") == "201.2mm"
48 |
49 | def test_file_save(self, tmp_path):
50 | generator = QRPlatbaGenerator(**self.data)
51 | img = generator.make_image()
52 |
53 | filename = tmp_path / "example.svg"
54 |
55 | img.save(filename)
56 | assert filename.exists()
57 |
58 | root = ET.parse(filename).getroot()
59 | assert root is not None
60 | viewbox = root.get("viewBox")
61 | assert viewbox is not None
62 |
63 | data = map(Decimal, viewbox.split(" "))
64 | assert list(data) == [Decimal(0), Decimal(0), Decimal("59.4"), Decimal("60.36")]
65 |
--------------------------------------------------------------------------------