qrid 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
qrid-1.0.0/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ .pytest_cache/
qrid-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,151 @@
1
+ Metadata-Version: 2.4
2
+ Name: qrid
3
+ Version: 1.0.0
4
+ Summary: Decode and encode MergeID electronic invoice QR codes
5
+ Project-URL: Homepage, https://github.com/Quality-XP-Development-SESSA/qrid-python
6
+ Project-URL: Repository, https://github.com/Quality-XP-Development-SESSA/qrid-python
7
+ Author-email: Valentin Secades Mendez <vsecades@qxdev.com>
8
+ License: MIT
9
+ Keywords: codec,decoder,invoice,mergeid,qr,qrcode
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.9
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest>=7.0; extra == 'dev'
21
+ Requires-Dist: segno>=1.6; extra == 'dev'
22
+ Provides-Extra: encode
23
+ Requires-Dist: segno>=1.6; extra == 'encode'
24
+ Description-Content-Type: text/markdown
25
+
26
+ # qrid — Python
27
+
28
+ Decode and encode **MergeID electronic invoice QR codes**.
29
+
30
+ Python port of [`qrid/codec`](https://packagist.org/packages/qrid/codec) (PHP). All three implementations (PHP, Node.js, Python) share the same payload format and function signatures, so QR codes generated by any one of them scan correctly in the others.
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ # Decode only (no extra dependencies)
36
+ pip install qrid
37
+
38
+ # Decode + encode SVG
39
+ pip install 'qrid[encode]'
40
+ ```
41
+
42
+ ## Usage
43
+
44
+ ### Decode
45
+
46
+ ```python
47
+ from qrid import decode_qr_id
48
+
49
+ # `encoded` is the raw string value scanned from a MergeID QR code
50
+ payload = decode_qr_id(encoded)
51
+
52
+ print(payload["v"]) # Payload schema version (int, currently 1)
53
+ print(payload["code"]) # Installation / activity code (e.g. "ACT-001")
54
+ print(payload["id"]) # Tax or company ID (e.g. "3101679980")
55
+ print(payload["company"]) # Company legal name
56
+ print(payload["email"]) # Billing e-mail address
57
+ print(payload["address"]) # Physical address
58
+ ```
59
+
60
+ `decode_qr_id` strips surrounding whitespace before decoding, so strings
61
+ copied with accidental padding are handled transparently.
62
+
63
+ **Exceptions raised:**
64
+
65
+ | Exception | Cause |
66
+ | --- | --- |
67
+ | `ValueError` | Input is not valid base64 |
68
+ | `json.JSONDecodeError` | Decoded bytes are not valid JSON |
69
+
70
+ ```python
71
+ import json
72
+ from qrid import decode_qr_id
73
+
74
+ try:
75
+ payload = decode_qr_id(raw)
76
+ except ValueError:
77
+ # QR data was not base64
78
+ ...
79
+ except json.JSONDecodeError:
80
+ # QR data decoded but was not the expected JSON structure
81
+ ...
82
+ ```
83
+
84
+ ### Encode (requires `qrid[encode]`)
85
+
86
+ ```python
87
+ from qrid import encode_qr_id
88
+
89
+ svg = encode_qr_id(
90
+ code="ACT-001",
91
+ id="3101679980",
92
+ company="Acme Corp S.A.",
93
+ email="billing@acme.example",
94
+ address="123 Main St, San José, Costa Rica",
95
+ )
96
+
97
+ # Write to a file
98
+ with open("invoice_qr.svg", "w") as f:
99
+ f.write(svg)
100
+
101
+ # Or serve directly
102
+ # Content-Type: image/svg+xml
103
+ ```
104
+
105
+ **Exceptions raised:**
106
+
107
+ | Exception | Cause |
108
+ | --- | --- |
109
+ | `ImportError` | `segno` is not installed (`pip install 'qrid[encode]'`) |
110
+
111
+ ## Payload format
112
+
113
+ The QR code data is a UTF-8 JSON object encoded as standard base64 (no line-breaks):
114
+
115
+ ```json
116
+ {
117
+ "v": 1,
118
+ "code": "ACT-001",
119
+ "id": "3101679980",
120
+ "company": "Acme Corp S.A.",
121
+ "email": "billing@acme.example",
122
+ "address": "123 Main St, San José, Costa Rica"
123
+ }
124
+ ```
125
+
126
+ | Field | Type | Description |
127
+ | --- | --- | --- |
128
+ | `v` | `int` | Payload schema version. Currently always `1`. |
129
+ | `code` | `str` | Installation or activity code that links the QR to an internal record. |
130
+ | `id` | `str` | Tax / company registration ID. |
131
+ | `company` | `str` | Legal company name (UTF-8, including accented characters). |
132
+ | `email` | `str` | Primary billing or contact e-mail address. |
133
+ | `address` | `str` | Physical address of the company. |
134
+
135
+ ## Requirements
136
+
137
+ | Dependency | Version | Required for |
138
+ | --- | --- | --- |
139
+ | Python | `>= 3.9` | Always |
140
+ | `segno` | `>= 1.6` | `encode_qr_id()` only |
141
+
142
+ ## Running tests
143
+
144
+ ```bash
145
+ pip install 'qrid[dev]'
146
+ pytest
147
+ ```
148
+
149
+ ## License
150
+
151
+ MIT
qrid-1.0.0/README.md ADDED
@@ -0,0 +1,126 @@
1
+ # qrid — Python
2
+
3
+ Decode and encode **MergeID electronic invoice QR codes**.
4
+
5
+ Python port of [`qrid/codec`](https://packagist.org/packages/qrid/codec) (PHP). All three implementations (PHP, Node.js, Python) share the same payload format and function signatures, so QR codes generated by any one of them scan correctly in the others.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ # Decode only (no extra dependencies)
11
+ pip install qrid
12
+
13
+ # Decode + encode SVG
14
+ pip install 'qrid[encode]'
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ### Decode
20
+
21
+ ```python
22
+ from qrid import decode_qr_id
23
+
24
+ # `encoded` is the raw string value scanned from a MergeID QR code
25
+ payload = decode_qr_id(encoded)
26
+
27
+ print(payload["v"]) # Payload schema version (int, currently 1)
28
+ print(payload["code"]) # Installation / activity code (e.g. "ACT-001")
29
+ print(payload["id"]) # Tax or company ID (e.g. "3101679980")
30
+ print(payload["company"]) # Company legal name
31
+ print(payload["email"]) # Billing e-mail address
32
+ print(payload["address"]) # Physical address
33
+ ```
34
+
35
+ `decode_qr_id` strips surrounding whitespace before decoding, so strings
36
+ copied with accidental padding are handled transparently.
37
+
38
+ **Exceptions raised:**
39
+
40
+ | Exception | Cause |
41
+ | --- | --- |
42
+ | `ValueError` | Input is not valid base64 |
43
+ | `json.JSONDecodeError` | Decoded bytes are not valid JSON |
44
+
45
+ ```python
46
+ import json
47
+ from qrid import decode_qr_id
48
+
49
+ try:
50
+ payload = decode_qr_id(raw)
51
+ except ValueError:
52
+ # QR data was not base64
53
+ ...
54
+ except json.JSONDecodeError:
55
+ # QR data decoded but was not the expected JSON structure
56
+ ...
57
+ ```
58
+
59
+ ### Encode (requires `qrid[encode]`)
60
+
61
+ ```python
62
+ from qrid import encode_qr_id
63
+
64
+ svg = encode_qr_id(
65
+ code="ACT-001",
66
+ id="3101679980",
67
+ company="Acme Corp S.A.",
68
+ email="billing@acme.example",
69
+ address="123 Main St, San José, Costa Rica",
70
+ )
71
+
72
+ # Write to a file
73
+ with open("invoice_qr.svg", "w") as f:
74
+ f.write(svg)
75
+
76
+ # Or serve directly
77
+ # Content-Type: image/svg+xml
78
+ ```
79
+
80
+ **Exceptions raised:**
81
+
82
+ | Exception | Cause |
83
+ | --- | --- |
84
+ | `ImportError` | `segno` is not installed (`pip install 'qrid[encode]'`) |
85
+
86
+ ## Payload format
87
+
88
+ The QR code data is a UTF-8 JSON object encoded as standard base64 (no line-breaks):
89
+
90
+ ```json
91
+ {
92
+ "v": 1,
93
+ "code": "ACT-001",
94
+ "id": "3101679980",
95
+ "company": "Acme Corp S.A.",
96
+ "email": "billing@acme.example",
97
+ "address": "123 Main St, San José, Costa Rica"
98
+ }
99
+ ```
100
+
101
+ | Field | Type | Description |
102
+ | --- | --- | --- |
103
+ | `v` | `int` | Payload schema version. Currently always `1`. |
104
+ | `code` | `str` | Installation or activity code that links the QR to an internal record. |
105
+ | `id` | `str` | Tax / company registration ID. |
106
+ | `company` | `str` | Legal company name (UTF-8, including accented characters). |
107
+ | `email` | `str` | Primary billing or contact e-mail address. |
108
+ | `address` | `str` | Physical address of the company. |
109
+
110
+ ## Requirements
111
+
112
+ | Dependency | Version | Required for |
113
+ | --- | --- | --- |
114
+ | Python | `>= 3.9` | Always |
115
+ | `segno` | `>= 1.6` | `encode_qr_id()` only |
116
+
117
+ ## Running tests
118
+
119
+ ```bash
120
+ pip install 'qrid[dev]'
121
+ pytest
122
+ ```
123
+
124
+ ## License
125
+
126
+ MIT
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "qrid"
7
+ version = "1.0.0"
8
+ description = "Decode and encode MergeID electronic invoice QR codes"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "Valentin Secades Mendez", email = "vsecades@qxdev.com" },
14
+ ]
15
+ keywords = ["qrcode", "qr", "invoice", "mergeid", "codec", "decoder"]
16
+ classifiers = [
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.9",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Operating System :: OS Independent",
24
+ "Typing :: Typed",
25
+ ]
26
+
27
+ [project.optional-dependencies]
28
+ encode = ["segno>=1.6"]
29
+ dev = ["pytest>=7.0", "segno>=1.6"]
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/Quality-XP-Development-SESSA/qrid-python"
33
+ Repository = "https://github.com/Quality-XP-Development-SESSA/qrid-python"
@@ -0,0 +1,3 @@
1
+ from .codec import decode_qr_id, encode_qr_id, QRIdPayload
2
+
3
+ __all__ = ["decode_qr_id", "encode_qr_id", "QRIdPayload"]
@@ -0,0 +1,90 @@
1
+ import base64
2
+ import binascii
3
+ import io
4
+ import json
5
+ from typing import TypedDict
6
+
7
+
8
+ class QRIdPayload(TypedDict):
9
+ v: int
10
+ code: str
11
+ id: str
12
+ company: str
13
+ email: str
14
+ address: str
15
+
16
+
17
+ def decode_qr_id(encoded: str) -> QRIdPayload:
18
+ """Decode a base64-encoded QR ID string into its structured payload.
19
+
20
+ Mirrors QRId\\Codec::decodeQRId() in qrid/codec (PHP).
21
+
22
+ Args:
23
+ encoded: The base64 string read from a MergeID QR code.
24
+
25
+ Returns:
26
+ A TypedDict with keys: v, code, id, company, email, address.
27
+
28
+ Raises:
29
+ ValueError: When the base64 input is malformed.
30
+ json.JSONDecodeError: When the decoded content is not valid JSON.
31
+ """
32
+ trimmed = encoded.strip()
33
+ try:
34
+ json_bytes = base64.b64decode(trimmed, validate=True)
35
+ except binascii.Error:
36
+ raise ValueError(
37
+ "Invalid base64 input: could not decode the provided string."
38
+ )
39
+ return json.loads(json_bytes.decode("utf-8"))
40
+
41
+
42
+ def encode_qr_id(
43
+ code: str,
44
+ id: str,
45
+ company: str,
46
+ email: str,
47
+ address: str,
48
+ ) -> str:
49
+ """Encode invoice identity fields and return an SVG QR code string.
50
+
51
+ Mirrors QRId\\Codec::encodeQRId() in qrid/codec (PHP).
52
+ Requires the encode extra: pip install 'qrid[encode]'
53
+
54
+ Args:
55
+ code: Installation or activity code.
56
+ id: Tax / company registration ID.
57
+ company: Company legal name (UTF-8).
58
+ email: Primary billing or contact e-mail address.
59
+ address: Physical address of the company.
60
+
61
+ Returns:
62
+ An SVG markup string containing the generated QR code.
63
+
64
+ Raises:
65
+ ImportError: When the segno package is not installed.
66
+ """
67
+ try:
68
+ import segno
69
+ except ImportError:
70
+ raise ImportError(
71
+ "segno is required for encode_qr_id(). "
72
+ "Install with: pip install 'qrid[encode]'"
73
+ )
74
+
75
+ payload: QRIdPayload = {
76
+ "v": 1,
77
+ "code": code,
78
+ "id": id,
79
+ "company": company,
80
+ "email": email,
81
+ "address": address,
82
+ }
83
+ encoded = base64.b64encode(
84
+ json.dumps(payload, ensure_ascii=False).encode("utf-8")
85
+ ).decode()
86
+
87
+ qr = segno.make_qr(encoded)
88
+ buf = io.BytesIO()
89
+ qr.save(buf, kind="svg", nl=False)
90
+ return buf.getvalue().decode("utf-8")
File without changes
@@ -0,0 +1,92 @@
1
+ import base64
2
+ import json
3
+
4
+ import pytest
5
+
6
+ from qrid import QRIdPayload, decode_qr_id, encode_qr_id
7
+
8
+
9
+ # ── helpers ───────────────────────────────────────────────────────────────────
10
+
11
+ def encode(payload: dict) -> str:
12
+ return base64.b64encode(
13
+ json.dumps(payload, ensure_ascii=False).encode("utf-8")
14
+ ).decode()
15
+
16
+
17
+ def sample_payload(**overrides) -> QRIdPayload:
18
+ p: QRIdPayload = {
19
+ "v": 1,
20
+ "code": "ACT-001",
21
+ "id": "3101679980",
22
+ "company": "Acme Corp S.A.",
23
+ "email": "billing@acme.example",
24
+ "address": "123 Main St, San José, Costa Rica",
25
+ }
26
+ p.update(overrides) # type: ignore[typeddict-item]
27
+ return p
28
+
29
+
30
+ # ── decode_qr_id ──────────────────────────────────────────────────────────────
31
+
32
+ class TestDecodeQRId:
33
+ def test_returns_all_fields(self):
34
+ result = decode_qr_id(encode(sample_payload()))
35
+
36
+ assert result["v"] == 1
37
+ assert result["code"] == "ACT-001"
38
+ assert result["id"] == "3101679980"
39
+ assert result["company"] == "Acme Corp S.A."
40
+ assert result["email"] == "billing@acme.example"
41
+ assert result["address"] == "123 Main St, San José, Costa Rica"
42
+
43
+ def test_handles_utf8(self):
44
+ result = decode_qr_id(
45
+ encode(sample_payload(
46
+ company="Société Générale",
47
+ address="Paseo Colón, San José, Costa Rica",
48
+ ))
49
+ )
50
+
51
+ assert result["company"] == "Société Générale"
52
+ assert result["address"] == "Paseo Colón, San José, Costa Rica"
53
+
54
+ def test_trims_surrounding_whitespace(self):
55
+ result = decode_qr_id(" " + encode(sample_payload()) + " ")
56
+ assert result["code"] == "ACT-001"
57
+
58
+ def test_raises_value_error_on_invalid_base64(self):
59
+ with pytest.raises(ValueError, match="Invalid base64 input"):
60
+ decode_qr_id("not-valid-base64!!!")
61
+
62
+ def test_raises_on_non_json_payload(self):
63
+ with pytest.raises(json.JSONDecodeError):
64
+ decode_qr_id(base64.b64encode(b"not json").decode())
65
+
66
+
67
+ # ── encode_qr_id ──────────────────────────────────────────────────────────────
68
+
69
+ class TestEncodeQRId:
70
+ def test_returns_svg_string(self):
71
+ svg = encode_qr_id(
72
+ code="ACT-001",
73
+ id="3101679980",
74
+ company="Acme Corp S.A.",
75
+ email="billing@acme.example",
76
+ address="123 Main St",
77
+ )
78
+
79
+ assert isinstance(svg, str)
80
+ assert "<svg" in svg.lower()
81
+
82
+ def test_round_trip(self):
83
+ p = sample_payload()
84
+ encoded = base64.b64encode(
85
+ json.dumps(p, ensure_ascii=False).encode("utf-8")
86
+ ).decode()
87
+
88
+ decoded = decode_qr_id(encoded)
89
+
90
+ assert decoded["code"] == p["code"]
91
+ assert decoded["id"] == p["id"]
92
+ assert decoded["company"] == p["company"]