Skip to content

Commit

Permalink
Flake test and black code formatting (#10)
Browse files Browse the repository at this point in the history
* Files formatting using black
* Added ruff for flake tests
* Added pre-commit hooks
* Dropped support for Python 3.7
  • Loading branch information
ViktorStiskala authored Apr 5, 2023
1 parent c5fe695 commit 25c0ba1
Show file tree
Hide file tree
Showing 11 changed files with 430 additions and 109 deletions.
3 changes: 2 additions & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ indent_style = space
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true
max_line_length = 120

[*.yml]
[{*.yml,*.yaml}]
indent_size = 2

[Makefile]
Expand Down
8 changes: 7 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [ "3.7", "3.8", "3.9", "3.10", "3.11" ]
python-version: [ "3.8", "3.9", "3.10", "3.11" ]
defaults:
run:
shell: bash
Expand Down Expand Up @@ -75,6 +75,12 @@ jobs:
- name: Run pytest
run: poetry run pytest -v

- name: Run ruff
run: poetry run ruff .

- name: Run black
run: poetry run black --check .

- name: Check for clean working tree
run: |
git diff --exit-code --stat HEAD
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
__pycache__
__pycache__/
*.pyc
dist/
.idea/
qrplatba.egg-info/
.*_cache/
10 changes: 10 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
repos:
- repo: https://github.com/psf/black
rev: 23.3.0
hooks:
- id: black
language_version: python3.11
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: 'v0.0.260'
hooks:
- id: ruff
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,5 +63,10 @@ This software is licensed under [MIT license](https://opensource.org/license/mit
- Changed license to MIT
- Added unit tests

### `1.1.0` (5 April 2023)

- Dropped support for Python 3.7
- Added pre-commit, black and ruff for code formatting



316 changes: 292 additions & 24 deletions poetry.lock

Large diffs are not rendered by default.

18 changes: 12 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
[tool.poetry]
name = "qrplatba"
version = "1.0.0"
version = "1.1.0"
description = "QR platba SVG QR code and SPAYD string generator."
authors = ["Viktor Stískala <[email protected]>"]
repository = "https://github.com/ViktorStiskala/python-qrplatba"
classifiers=[
'Intended Audience :: Developers',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
Expand All @@ -20,18 +19,25 @@ license = "MIT"
readme = "README.md"

[tool.poetry.dependencies]
python = "^3.7"
python = "^3.8"
qrcode = "^7.4"

[tool.poetry.dev-dependencies]

[tool.poetry.group.test]
optional = true

[tool.poetry.group.test.dependencies]
pytest = "^7.2"
pytest-github-actions-annotate-failures = "^0.1.8"
ruff = "^0.0.261"
black = "^23.3.0"

[tool.poetry.group.dev.dependencies]
pre-commit = "^3.2.2"

[tool.ruff]
line-length = 120

[tool.black]
line-length = 120

[build-system]
requires = ["poetry-core>=1.0.0"]
Expand Down
2 changes: 2 additions & 0 deletions qrplatba/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
from .spayd import QRPlatbaGenerator

__all__ = ["QRPlatbaGenerator"]
85 changes: 51 additions & 34 deletions qrplatba/spayd.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,32 @@


class QRPlatbaGenerator:
RE_ACCOUNT = re.compile(r'((?P<ba>\d+(?=-))-)?(?P<a>\d+)/(?P<b>\d{4})')

def __init__(self, account, amount=None, currency=None, x_vs=None, x_ss=None, x_ks=None, alternate_accounts=None, recipient_name=None,
due_date=None, payment_type=None, message=None, notification_type=None, notification_address=None, x_per=None, x_id=None,
x_url=None, reference=None):
RE_ACCOUNT = re.compile(r"((?P<ba>\d+(?=-))-)?(?P<a>\d+)/(?P<b>\d{4})")

def __init__(
self,
account,
amount=None,
currency=None,
x_vs=None,
x_ss=None,
x_ks=None,
alternate_accounts=None,
recipient_name=None,
due_date=None,
payment_type=None,
message=None,
notification_type=None,
notification_address=None,
x_per=None,
x_id=None,
x_url=None,
reference=None,
):
"""
http://qr-platba.cz/pro-vyvojare/specifikace-formatu/
:param account: ACC account number, can be specified either as IBAN or in CZ commonly used format 12-123456789/0300
:param account: ACC account number, can be specified either as IBAN or in CZ format 12-123456789/0300
:param amount: AM payment amount
:param currency: CC currency (3 digits)
:param x_vs: X-VS
Expand Down Expand Up @@ -54,14 +71,14 @@ def _convert_to_iban(self, account):
Convert czech account number to IBAN
"""
acc = self.RE_ACCOUNT.match(account)
iban = 'CZ00{b}{ba:0>6}{a:0>10}'.format(
ba=acc.group('ba') or 0,
a=acc.group('a'),
b=acc.group('b'),
iban = "CZ00{b}{ba:0>6}{a:0>10}".format(
ba=acc.group("ba") or 0,
a=acc.group("a"),
b=acc.group("b"),
)

# convert IBAN letters into numbers
crc = re.sub(r'[A-Z]', lambda m: str(ord(m.group(0)) - 55), iban[4:] + iban[:4])
crc = re.sub(r"[A-Z]", lambda m: str(ord(m.group(0)) - 55), iban[4:] + iban[:4])

# compute control digits
digits = "{:0>2}".format(98 - int(crc) % 97)
Expand All @@ -71,7 +88,7 @@ def _convert_to_iban(self, account):
@property
def _account(self):
if self.account is not None:
out = 'ACC:{}*'
out = "ACC:{}*"

if self.RE_ACCOUNT.match(self.account):
return out.format(self._convert_to_iban(self.account))
Expand All @@ -87,8 +104,8 @@ def _alternate_accounts(self):
formatted.append(self._convert_to_iban(account))
else:
formatted.append(account)
return "ALT-ACC:{}*".format(','.join(formatted))
return ''
return "ALT-ACC:{}*".format(",".join(formatted))
return ""

@property
def _amount(self):
Expand All @@ -99,47 +116,47 @@ def _amount(self):
@property
def _due_date(self):
if self.due_date is not None:
str_part = 'DT:{}*'
str_part = "DT:{}*"
if isinstance(self.due_date, datetime):
return str_part.format(self.due_date.date().isoformat()).replace('-', '')
return str_part.format(self.due_date.date().isoformat()).replace("-", "")
if isinstance(self.due_date, date):
return str_part.format(self.due_date.isoformat()).replace('-', '')
return str_part.format(self.due_date.isoformat()).replace("-", "")
return str_part.format(self.due_date)
return ''
return ""

def _format_item_string(self, item, name):
if item:
return "{name}:{value}*".format(name=name, value=item)
return ''
return ""

def get_text(self):
return "SPD*1.0*{ACC}{ALTACC}{AM}{CC}{RF}{RN}{DT}{PT}{MSG}{NT}{NTA}{XPER}{XVS}{XSS}{XKS}{XID}{XURL}".format(
ACC=self._account,
ALTACC=self._alternate_accounts,
AM=self._amount,
CC=self._format_item_string(self.currency, 'CC'),
RF=self._format_item_string(self.reference, 'RF'),
RN=self._format_item_string(self.recipient_name, 'RN'),
CC=self._format_item_string(self.currency, "CC"),
RF=self._format_item_string(self.reference, "RF"),
RN=self._format_item_string(self.recipient_name, "RN"),
DT=self._due_date,
PT=self._format_item_string(self.payment_type, 'PT'),
MSG=self._format_item_string(self.message, 'MSG'),
NT=self._format_item_string(self.notification_type, 'NT'),
NTA=self._format_item_string(self.notification_address, 'NTA'),
XPER=self._format_item_string(self.x_per, 'X-PER'),
XVS=self._format_item_string(self.x_vs, 'X-VS'),
XSS=self._format_item_string(self.x_ss, 'X-SS'),
XKS=self._format_item_string(self.x_ks, 'X-KS'),
XID=self._format_item_string(self.x_id, 'X-ID'),
XURL=self._format_item_string(self.x_url, 'X-URL')
).rstrip('*')
PT=self._format_item_string(self.payment_type, "PT"),
MSG=self._format_item_string(self.message, "MSG"),
NT=self._format_item_string(self.notification_type, "NT"),
NTA=self._format_item_string(self.notification_address, "NTA"),
XPER=self._format_item_string(self.x_per, "X-PER"),
XVS=self._format_item_string(self.x_vs, "X-VS"),
XSS=self._format_item_string(self.x_ss, "X-SS"),
XKS=self._format_item_string(self.x_ks, "X-KS"),
XID=self._format_item_string(self.x_id, "X-ID"),
XURL=self._format_item_string(self.x_url, "X-URL"),
).rstrip("*")

def make_image(self, border=2, box_size=12, error_correction=qrcode.constants.ERROR_CORRECT_M):
qr = qrcode.QRCode(
version=None,
error_correction=error_correction,
image_factory=QRPlatbaSVGImage,
border=border,
box_size=box_size
box_size=box_size,
)
qr.add_data(self.get_text())
qr.make(fit=True)
Expand Down
53 changes: 29 additions & 24 deletions qrplatba/svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ class QRPlatbaSVGImage(svg.SvgPathImage):
text size is computed to achieve width of 16 QR points.
"""

QR_TEXT_STYLE = 'font-size:{size}px;font-weight:bold;fill:#000000;font-family:Arial;'
FONT_SIZE = Decimal('3.5')
FONT_HEIGHT = Decimal('8')
QR_TEXT_STYLE = "font-size:{size}px;font-weight:bold;fill:#000000;font-family:Arial;"
FONT_SIZE = Decimal("3.5")
FONT_HEIGHT = Decimal("8")

LINE_SIZE = Decimal('0.25')
LINE_SIZE = Decimal("0.25")
INSIDE_BORDER = 4

BOTTOM_LINE_SEGMENTS = (2, 22)
Expand All @@ -41,33 +41,33 @@ def _get_scaled_sizes(self):
scale_ratio = self.units(self.box_size, text=False)

def strip_zeros(value):
return Decimal(str(value).rstrip('0').rstrip('.')) if '.' in str(value) else value
return Decimal(str(value).rstrip("0").rstrip(".")) if "." in str(value) else value

return ScaledSizes(
inside_border=strip_zeros(self.INSIDE_BORDER * scale_ratio),
outside_border=strip_zeros(self.outside_border * scale_ratio),
width=strip_zeros(self.width * scale_ratio),
line_size=strip_zeros(self.LINE_SIZE * scale_ratio),
ratio=scale_ratio
ratio=scale_ratio,
)

def make_border(self):
"""Creates black thin border around QR code"""
scaled = self._get_scaled_sizes()

def sizes(o, i, w, l): # size helper
return o * scaled.outside_border + i * scaled.inside_border + w * scaled.width + l * scaled.line_size
def sizes(ob, ib, wd, ln): # size helper
return ob * scaled.outside_border + ib * scaled.inside_border + wd * scaled.width + ln * scaled.line_size

horizontal_line = 'M{x0},{y0}h{length}v{width}h-{length}z'
vertical_line = 'M{x0},{y0}v{length}h{width}v-{length}z'
horizontal_line = "M{x0},{y0}h{length}v{width}h-{length}z"
vertical_line = "M{x0},{y0}v{length}h{width}v-{length}z"

def get_subpaths():
# top line
yield horizontal_line.format(
x0=scaled.outside_border,
y0=scaled.outside_border,
length=sizes(0, 2, 1, 2),
width=scaled.line_size
width=scaled.line_size,
)

b_first, b_second = self.BOTTOM_LINE_SEGMENTS
Expand All @@ -77,46 +77,52 @@ def get_subpaths():
x0=scaled.outside_border,
y0=sizes(1, 2, 1, 1),
length=b_first * scaled.ratio,
width=scaled.line_size
width=scaled.line_size,
)

# bottom line - second segment
yield horizontal_line.format(
x0=scaled.outside_border + b_second * scaled.ratio,
y0=sizes(1, 2, 1, 1),
length=sizes(0, 2, 1, 2) - b_second * scaled.ratio,
width=scaled.line_size
width=scaled.line_size,
)

# left line
yield vertical_line.format(
x0=scaled.outside_border,
y0=scaled.outside_border + scaled.line_size,
length=scaled.width + 2 * scaled.inside_border,
width=scaled.line_size
width=scaled.line_size,
)

# right line
yield vertical_line.format(
x0=sizes(1, 2, 1, 1),
y0=sizes(1, 0, 0, 1),
length=sizes(0, 2, 1, 0),
width=scaled.line_size
width=scaled.line_size,
)

subpaths = ' '.join(get_subpaths())
return ET.Element('path', d=subpaths, id="qrplatba-border", **self.QR_PATH_STYLE)
subpaths = " ".join(get_subpaths())
return ET.Element("path", d=subpaths, id="qrplatba-border", **self.QR_PATH_STYLE)

def make_text(self):
"""Creates "QR platba" text element"""
scaled = self._get_scaled_sizes()
text_style = self.QR_TEXT_STYLE.format(size=(self.FONT_SIZE * scaled.ratio).quantize(Decimal('0.01')))
text_style = self.QR_TEXT_STYLE.format(size=(self.FONT_SIZE * scaled.ratio).quantize(Decimal("0.01")))

x_pos = str(scaled.outside_border + scaled.line_size + 4 * scaled.ratio)
y_pos = str(scaled.outside_border + scaled.line_size + 2 * scaled.inside_border + scaled.width + (self.FONT_HEIGHT / 4) * scaled.ratio)
y_pos = str(
scaled.outside_border
+ scaled.line_size
+ 2 * scaled.inside_border
+ scaled.width
+ (self.FONT_HEIGHT / 4) * scaled.ratio
)

text_el = ET.Element('text', style=text_style, x=x_pos, y=y_pos, id="qrplatba-text")
text_el.text = 'QR platba'
text_el = ET.Element("text", style=text_style, x=x_pos, y=y_pos, id="qrplatba-text")
text_el.text = "QR platba"

return text_el

Expand All @@ -126,14 +132,13 @@ def _svg(self, viewBox=None, **kwargs):

box = "0 0 {w} {h}".format(
w=self.units(self.pixel_size, text=False),
h=self.units(h_pixels, text=False)
h=self.units(h_pixels, text=False),
)
svg_el = super()._svg(viewBox=box, **kwargs)
svg_el.append(self.make_border())
svg_el.append(self.make_text())

# update size of the SVG element
svg_el.attrib['height'] = str(self.units(h_pixels))
svg_el.attrib["height"] = str(self.units(h_pixels))

return svg_el

Loading

0 comments on commit 25c0ba1

Please sign in to comment.