barcodekit 0.1.2__py3-none-win_amd64.whl

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.
barcodekit/__init__.py ADDED
@@ -0,0 +1,201 @@
1
+ """Generate barcode PNGs with the local barcode-rest CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ._core import BarcodeImage, BarcodeKit
6
+ from ._errors import (
7
+ BarcodeKitBinaryNotFound,
8
+ BarcodeKitCommandError,
9
+ BarcodeKitError,
10
+ BarcodeKitTimeout,
11
+ BarcodeKitUnsupportedPlatform,
12
+ )
13
+ from ._version import __version__
14
+
15
+ _default_kit = BarcodeKit()
16
+
17
+
18
+ def generate(symbology: str, text: str, **options: object) -> BarcodeImage:
19
+ return _default_kit.generate(symbology, text, **options)
20
+
21
+
22
+ def datamatrix(
23
+ text: str,
24
+ *,
25
+ size: int | None = 256,
26
+ module: int | None = None,
27
+ quiet: int | None = None,
28
+ ) -> BarcodeImage:
29
+ return _default_kit.datamatrix(text, size=size, module=module, quiet=quiet)
30
+
31
+
32
+ def qr(
33
+ text: str,
34
+ *,
35
+ size: int | None = 512,
36
+ module: int | None = None,
37
+ quiet: int | None = None,
38
+ level: str = "M",
39
+ ) -> BarcodeImage:
40
+ return _default_kit.qr(text, size=size, module=module, quiet=quiet, level=level)
41
+
42
+
43
+ def aztec(
44
+ text: str,
45
+ *,
46
+ size: int | None = 512,
47
+ module: int | None = None,
48
+ quiet: int | None = None,
49
+ ) -> BarcodeImage:
50
+ return _default_kit.aztec(text, size=size, module=module, quiet=quiet)
51
+
52
+
53
+ def pdf417(
54
+ text: str,
55
+ *,
56
+ module: int | None = None,
57
+ quiet: int | None = None,
58
+ level: int | str = 2,
59
+ ) -> BarcodeImage:
60
+ return _default_kit.pdf417(text, module=module, quiet=quiet, level=level)
61
+
62
+
63
+ def code128(
64
+ text: str,
65
+ *,
66
+ module: int | None = None,
67
+ height: int | None = 80,
68
+ quiet: int | None = None,
69
+ label: bool = False,
70
+ ) -> BarcodeImage:
71
+ return _default_kit.code128(
72
+ text, module=module, height=height, quiet=quiet, label=label
73
+ )
74
+
75
+
76
+ def code39(
77
+ text: str,
78
+ *,
79
+ module: int | None = None,
80
+ height: int | None = 80,
81
+ quiet: int | None = None,
82
+ label: bool = False,
83
+ fullascii: bool = False,
84
+ ) -> BarcodeImage:
85
+ return _default_kit.code39(
86
+ text,
87
+ module=module,
88
+ height=height,
89
+ quiet=quiet,
90
+ label=label,
91
+ fullascii=fullascii,
92
+ )
93
+
94
+
95
+ def code93(
96
+ text: str,
97
+ *,
98
+ module: int | None = None,
99
+ height: int | None = 80,
100
+ quiet: int | None = None,
101
+ label: bool = False,
102
+ fullascii: bool = False,
103
+ ) -> BarcodeImage:
104
+ return _default_kit.code93(
105
+ text,
106
+ module=module,
107
+ height=height,
108
+ quiet=quiet,
109
+ label=label,
110
+ fullascii=fullascii,
111
+ )
112
+
113
+
114
+ def codabar(
115
+ text: str,
116
+ *,
117
+ module: int | None = None,
118
+ height: int | None = 80,
119
+ quiet: int | None = None,
120
+ label: bool = False,
121
+ ) -> BarcodeImage:
122
+ return _default_kit.codabar(
123
+ text, module=module, height=height, quiet=quiet, label=label
124
+ )
125
+
126
+
127
+ def itf(
128
+ text: str,
129
+ *,
130
+ module: int | None = None,
131
+ height: int | None = 80,
132
+ quiet: int | None = None,
133
+ label: bool = False,
134
+ ) -> BarcodeImage:
135
+ return _default_kit.itf(
136
+ text, module=module, height=height, quiet=quiet, label=label
137
+ )
138
+
139
+
140
+ def code25(
141
+ text: str,
142
+ *,
143
+ module: int | None = None,
144
+ height: int | None = 80,
145
+ quiet: int | None = None,
146
+ label: bool = False,
147
+ ) -> BarcodeImage:
148
+ return _default_kit.code25(
149
+ text, module=module, height=height, quiet=quiet, label=label
150
+ )
151
+
152
+
153
+ def ean13(
154
+ text: str,
155
+ *,
156
+ module: int | None = None,
157
+ height: int | None = 80,
158
+ quiet: int | None = None,
159
+ label: bool = False,
160
+ ) -> BarcodeImage:
161
+ return _default_kit.ean13(
162
+ text, module=module, height=height, quiet=quiet, label=label
163
+ )
164
+
165
+
166
+ def ean8(
167
+ text: str,
168
+ *,
169
+ module: int | None = None,
170
+ height: int | None = 80,
171
+ quiet: int | None = None,
172
+ label: bool = False,
173
+ ) -> BarcodeImage:
174
+ return _default_kit.ean8(
175
+ text, module=module, height=height, quiet=quiet, label=label
176
+ )
177
+
178
+
179
+ __all__ = [
180
+ "BarcodeImage",
181
+ "BarcodeKit",
182
+ "BarcodeKitBinaryNotFound",
183
+ "BarcodeKitCommandError",
184
+ "BarcodeKitError",
185
+ "BarcodeKitTimeout",
186
+ "BarcodeKitUnsupportedPlatform",
187
+ "__version__",
188
+ "aztec",
189
+ "codabar",
190
+ "code25",
191
+ "code39",
192
+ "code93",
193
+ "code128",
194
+ "datamatrix",
195
+ "ean8",
196
+ "ean13",
197
+ "generate",
198
+ "itf",
199
+ "pdf417",
200
+ "qr",
201
+ ]
@@ -0,0 +1 @@
1
+
Binary file
barcodekit/_binary.py ADDED
@@ -0,0 +1,85 @@
1
+ """Resolve the barcode-rest executable for the current platform."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import platform
7
+ import stat
8
+ from pathlib import Path
9
+
10
+ from ._errors import BarcodeKitBinaryNotFound, BarcodeKitUnsupportedPlatform
11
+
12
+ _BINARY_ENV = "BARCODEKIT_BINARY"
13
+ _BINARY_DIR = Path(__file__).parent / "_bin"
14
+ _MACHINE_ALIASES = {
15
+ "amd64": "amd64",
16
+ "x86_64": "amd64",
17
+ "arm64": "arm64",
18
+ "aarch64": "arm64",
19
+ }
20
+ _SUPPORTED_PLATFORMS = {
21
+ ("windows", "amd64"): "barcode-rest.exe",
22
+ ("linux", "amd64"): "barcode-rest",
23
+ ("linux", "arm64"): "barcode-rest",
24
+ }
25
+
26
+
27
+ def _current_platform() -> tuple[str, str]:
28
+ system = platform.system().lower()
29
+ raw_machine = platform.machine().lower()
30
+ return system, _MACHINE_ALIASES.get(raw_machine, raw_machine)
31
+
32
+
33
+ def _bundled_binary_name() -> tuple[str, str]:
34
+ system, machine = _current_platform()
35
+ name = _SUPPORTED_PLATFORMS.get((system, machine))
36
+ if name is None:
37
+ supported = ", ".join(f"{os_name}/{arch}" for os_name, arch in _SUPPORTED_PLATFORMS)
38
+ raise BarcodeKitUnsupportedPlatform(
39
+ f"Unsupported platform {system}/{machine}. Supported platforms: {supported}."
40
+ )
41
+ return system, name
42
+
43
+
44
+ def _require_file(value: str | Path, source: str) -> Path:
45
+ path = Path(value).expanduser()
46
+ if not path.is_file():
47
+ raise BarcodeKitBinaryNotFound(f"{source} does not point to a file: {path}")
48
+ return path
49
+
50
+
51
+ def _ensure_bundled_executable(path: Path, system: str) -> None:
52
+ if system == "windows":
53
+ return
54
+ mode = path.stat().st_mode
55
+ if mode & stat.S_IXUSR:
56
+ return
57
+ try:
58
+ path.chmod(mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
59
+ except OSError as exc:
60
+ raise BarcodeKitBinaryNotFound(
61
+ f"Bundled binary is not executable and its permissions could not be changed: {path}"
62
+ ) from exc
63
+
64
+
65
+ def resolve_binary(executable: str | Path | None = None) -> Path:
66
+ """Resolve an explicit, environment-provided, or bundled executable."""
67
+ system, bundled_name = _bundled_binary_name()
68
+
69
+ if executable is not None:
70
+ return _require_file(executable, "The executable argument")
71
+
72
+ environment_path = os.environ.get(_BINARY_ENV)
73
+ if environment_path:
74
+ return _require_file(environment_path, _BINARY_ENV)
75
+
76
+ bundled = _BINARY_DIR / bundled_name
77
+ if not bundled.is_file():
78
+ raise BarcodeKitBinaryNotFound(
79
+ f"Bundled barcode-rest binary was not found at {bundled}. "
80
+ f"Install the platform-specific wheel, set {_BINARY_ENV}, "
81
+ "or pass executable= to BarcodeKit."
82
+ )
83
+ _ensure_bundled_executable(bundled, system)
84
+ return bundled
85
+
barcodekit/_core.py ADDED
@@ -0,0 +1,478 @@
1
+ """Core barcodekit API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ import re
7
+ import subprocess
8
+ from collections.abc import Mapping
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+
12
+ from ._binary import resolve_binary
13
+ from ._errors import (
14
+ BarcodeKitBinaryNotFound,
15
+ BarcodeKitCommandError,
16
+ BarcodeKitTimeout,
17
+ )
18
+
19
+ _PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
20
+ _TWO_DIMENSIONAL_LIMITS = {
21
+ "datamatrix": 128,
22
+ "qr": 256,
23
+ "aztec": 256,
24
+ "pdf417": 256,
25
+ }
26
+ _ONE_DIMENSIONAL = {
27
+ "code128",
28
+ "code39",
29
+ "code93",
30
+ "codabar",
31
+ "itf",
32
+ "code25",
33
+ "ean13",
34
+ "ean8",
35
+ }
36
+ _SUPPORTED_SYMBOLOGIES = frozenset(_TWO_DIMENSIONAL_LIMITS) | _ONE_DIMENSIONAL
37
+ _COMMON_2D_OPTIONS = frozenset({"module", "quiet", "size"})
38
+ _COMMON_1D_OPTIONS = frozenset({"module", "height", "quiet", "label"})
39
+ _ALLOWED_OPTIONS = {
40
+ "datamatrix": _COMMON_2D_OPTIONS,
41
+ "qr": _COMMON_2D_OPTIONS | {"level"},
42
+ "aztec": _COMMON_2D_OPTIONS,
43
+ "pdf417": {"module", "quiet", "level"},
44
+ "code128": _COMMON_1D_OPTIONS,
45
+ "code39": _COMMON_1D_OPTIONS | {"fullascii"},
46
+ "code93": _COMMON_1D_OPTIONS | {"fullascii"},
47
+ "codabar": _COMMON_1D_OPTIONS,
48
+ "itf": _COMMON_1D_OPTIONS,
49
+ "code25": _COMMON_1D_OPTIONS,
50
+ "ean13": _COMMON_1D_OPTIONS,
51
+ "ean8": _COMMON_1D_OPTIONS,
52
+ }
53
+ _BASIC_CODE39_CHARACTERS = frozenset("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. $/+%")
54
+ _CODABAR_PATTERN = re.compile(r"^[A-D][0-9\-$:/.+]+[A-D]$")
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class BarcodeImage:
59
+ """A generated PNG image."""
60
+
61
+ png: bytes
62
+
63
+ def save(self, path: str | Path) -> Path:
64
+ """Write the PNG bytes to path and return the resulting Path."""
65
+ destination = Path(path)
66
+ destination.write_bytes(self.png)
67
+ return destination
68
+
69
+ def to_bytes(self) -> bytes:
70
+ """Return the raw PNG bytes."""
71
+ return self.png
72
+
73
+
74
+ def _validate_int(value: object, name: str, minimum: int, maximum: int) -> int:
75
+ if type(value) is not int:
76
+ raise TypeError(f"{name} must be an integer")
77
+ integer = value
78
+ if not minimum <= integer <= maximum:
79
+ raise ValueError(f"{name} must be between {minimum} and {maximum}")
80
+ return integer
81
+
82
+
83
+ def _validate_bool(value: object, name: str) -> bool:
84
+ if type(value) is not bool:
85
+ raise TypeError(f"{name} must be a boolean")
86
+ return value
87
+
88
+
89
+ def _validate_qr_level(value: object) -> str:
90
+ if not isinstance(value, str):
91
+ raise TypeError("level must be a string for QR codes")
92
+ level = value.upper()
93
+ if level not in {"L", "M", "Q", "H"}:
94
+ raise ValueError("QR level must be one of L, M, Q, or H")
95
+ return level
96
+
97
+
98
+ def _validate_pdf417_level(value: object) -> int:
99
+ if isinstance(value, bool):
100
+ raise TypeError("level must be an integer or numeric string for PDF417")
101
+ if isinstance(value, str):
102
+ if not value.isdigit():
103
+ raise ValueError("PDF417 level must be between 0 and 8")
104
+ value = int(value)
105
+ if not isinstance(value, int):
106
+ raise TypeError("level must be an integer or numeric string for PDF417")
107
+ if not 0 <= value <= 8:
108
+ raise ValueError("PDF417 level must be between 0 and 8")
109
+ return value
110
+
111
+
112
+ def _validate_options(symbology: str, options: Mapping[str, object]) -> dict[str, object]:
113
+ unknown = set(options) - _ALLOWED_OPTIONS[symbology]
114
+ if unknown:
115
+ names = ", ".join(sorted(unknown))
116
+ raise ValueError(f"Unsupported option(s) for {symbology}: {names}")
117
+
118
+ if options.get("size") is not None and options.get("module") is not None:
119
+ raise ValueError(
120
+ "size and module cannot be specified together; set size=None to use module"
121
+ )
122
+
123
+ validated: dict[str, object] = {}
124
+ for name, value in options.items():
125
+ if value is None:
126
+ continue
127
+ if name == "module":
128
+ validated[name] = _validate_int(value, name, 2, 32)
129
+ elif name == "quiet":
130
+ validated[name] = _validate_int(value, name, 0, 16)
131
+ elif name == "size":
132
+ validated[name] = _validate_int(value, name, 16, 2048)
133
+ elif name == "height":
134
+ validated[name] = _validate_int(value, name, 20, 600)
135
+ elif name in {"label", "fullascii"}:
136
+ validated[name] = _validate_bool(value, name)
137
+ elif name == "level" and symbology == "qr":
138
+ validated[name] = _validate_qr_level(value)
139
+ elif name == "level" and symbology == "pdf417":
140
+ validated[name] = _validate_pdf417_level(value)
141
+ return validated
142
+
143
+
144
+ def _utf8_bytes(text: str) -> bytes:
145
+ try:
146
+ return text.encode("utf-8")
147
+ except UnicodeEncodeError as exc:
148
+ raise ValueError("text must be valid UTF-8") from exc
149
+
150
+
151
+ def _validate_ascii(text: str, symbology: str, maximum: int) -> None:
152
+ if not text.isascii():
153
+ raise ValueError(f"{symbology} text must contain ASCII characters only")
154
+ if len(text) > maximum:
155
+ raise ValueError(f"{symbology} text must be at most {maximum} characters")
156
+
157
+
158
+ def _ean_checksum(body: str) -> str:
159
+ total = sum(
160
+ int(digit) * (3 if index % 2 == 0 else 1)
161
+ for index, digit in enumerate(reversed(body))
162
+ )
163
+ return str((10 - total % 10) % 10)
164
+
165
+
166
+ def _validate_text(symbology: str, text: str, options: Mapping[str, object]) -> None:
167
+ if not isinstance(text, str):
168
+ raise TypeError("text must be a string")
169
+ if not text:
170
+ raise ValueError("text must not be empty")
171
+ if "\x00" in text:
172
+ raise ValueError("text must not contain NUL characters")
173
+
174
+ if symbology in _TWO_DIMENSIONAL_LIMITS:
175
+ maximum = _TWO_DIMENSIONAL_LIMITS[symbology]
176
+ if len(_utf8_bytes(text)) > maximum:
177
+ raise ValueError(f"{symbology} text must be at most {maximum} UTF-8 bytes")
178
+ return
179
+
180
+ if symbology == "code128":
181
+ _validate_ascii(text, symbology, 80)
182
+ return
183
+
184
+ if symbology in {"code39", "code93"}:
185
+ _validate_ascii(text, symbology, 128)
186
+ if not options.get("fullascii", False) and not set(text) <= _BASIC_CODE39_CHARACTERS:
187
+ raise ValueError(
188
+ f"{symbology} text contains characters that require fullascii=True"
189
+ )
190
+ return
191
+
192
+ if symbology == "codabar":
193
+ _validate_ascii(text, symbology, 128)
194
+ if _CODABAR_PATTERN.fullmatch(text) is None:
195
+ raise ValueError(
196
+ "codabar text must start and end with A-D and contain valid data characters"
197
+ )
198
+ return
199
+
200
+ if symbology in {"itf", "code25"}:
201
+ _validate_ascii(text, symbology, 128)
202
+ if not text.isdigit():
203
+ raise ValueError(f"{symbology} text must contain digits only")
204
+ if symbology == "itf" and len(text) % 2:
205
+ raise ValueError("itf text must contain an even number of digits")
206
+ return
207
+
208
+ required_lengths = (12, 13) if symbology == "ean13" else (7, 8)
209
+ if not text.isascii() or not text.isdigit() or len(text) not in required_lengths:
210
+ lengths = " or ".join(str(length) for length in required_lengths)
211
+ raise ValueError(f"{symbology} text must contain exactly {lengths} digits")
212
+ if len(text) == required_lengths[1] and text[-1] != _ean_checksum(text[:-1]):
213
+ raise ValueError(f"{symbology} text has an invalid check digit")
214
+
215
+
216
+ def _option_flag(name: str) -> str:
217
+ return f"--{name.replace('_', '-')}"
218
+
219
+
220
+ def _build_option_arguments(options: Mapping[str, object]) -> list[str]:
221
+ arguments: list[str] = []
222
+ for name, value in options.items():
223
+ flag = _option_flag(name)
224
+ if isinstance(value, bool):
225
+ if value:
226
+ arguments.append(flag)
227
+ else:
228
+ arguments.extend((flag, str(value)))
229
+ return arguments
230
+
231
+
232
+ def _redact_stderr(stderr: str, text: str) -> str:
233
+ return stderr.replace(text, "<redacted>") if text else stderr
234
+
235
+
236
+ def _is_png(data: bytes) -> bool:
237
+ if not data.startswith(_PNG_SIGNATURE):
238
+ return False
239
+
240
+ offset = len(_PNG_SIGNATURE)
241
+ first_chunk = True
242
+ while offset + 12 <= len(data):
243
+ length = int.from_bytes(data[offset : offset + 4], "big")
244
+ chunk_type = data[offset + 4 : offset + 8]
245
+ chunk_end = offset + 12 + length
246
+ if chunk_end > len(data):
247
+ return False
248
+ if first_chunk and (chunk_type != b"IHDR" or length != 13):
249
+ return False
250
+ first_chunk = False
251
+ if chunk_type == b"IEND":
252
+ return length == 0 and chunk_end == len(data)
253
+ offset = chunk_end
254
+ return False
255
+
256
+
257
+ class BarcodeKit:
258
+ """Generate barcode PNGs by invoking barcode-rest in one-shot CLI mode."""
259
+
260
+ def __init__(
261
+ self,
262
+ executable: str | Path | None = None,
263
+ timeout: float = 10.0,
264
+ ) -> None:
265
+ if isinstance(timeout, bool) or not isinstance(timeout, (int, float)):
266
+ raise TypeError("timeout must be a number")
267
+ if not math.isfinite(timeout) or timeout <= 0:
268
+ raise ValueError("timeout must be a finite number greater than zero")
269
+ self._executable = executable
270
+ self.timeout = float(timeout)
271
+
272
+ def generate(self, symbology: str, text: str, **options: object) -> BarcodeImage:
273
+ """Generate a barcode using a supported symbology."""
274
+ if not isinstance(symbology, str):
275
+ raise TypeError("symbology must be a string")
276
+ if symbology not in _SUPPORTED_SYMBOLOGIES:
277
+ raise ValueError(f"Unsupported symbology: {symbology}")
278
+
279
+ validated_options = _validate_options(symbology, options)
280
+ _validate_text(symbology, text, validated_options)
281
+ executable = resolve_binary(self._executable)
282
+ command = [
283
+ str(executable),
284
+ "generate",
285
+ symbology,
286
+ "--text",
287
+ text,
288
+ "--output",
289
+ "-",
290
+ *_build_option_arguments(validated_options),
291
+ ]
292
+
293
+ try:
294
+ completed = subprocess.run( # noqa: UP022 - explicit PIPEs are part of the contract
295
+ command,
296
+ stdout=subprocess.PIPE,
297
+ stderr=subprocess.PIPE,
298
+ timeout=self.timeout,
299
+ check=False,
300
+ shell=False,
301
+ )
302
+ except subprocess.TimeoutExpired as exc:
303
+ raise BarcodeKitTimeout(self.timeout, command) from exc
304
+ except FileNotFoundError as exc:
305
+ raise BarcodeKitBinaryNotFound(
306
+ f"barcode-rest executable disappeared before it could be run: {executable}"
307
+ ) from exc
308
+ except OSError as exc:
309
+ message = _redact_stderr(str(exc), text)
310
+ raise BarcodeKitCommandError(None, command, message) from exc
311
+
312
+ stderr = _redact_stderr(completed.stderr.decode("utf-8", errors="replace"), text)
313
+ if completed.returncode != 0:
314
+ raise BarcodeKitCommandError(completed.returncode, command, stderr)
315
+ if not _is_png(completed.stdout):
316
+ raise BarcodeKitCommandError(0, command, "barcode-rest produced invalid PNG output")
317
+ return BarcodeImage(completed.stdout)
318
+
319
+ def datamatrix(
320
+ self,
321
+ text: str,
322
+ *,
323
+ size: int | None = 256,
324
+ module: int | None = None,
325
+ quiet: int | None = None,
326
+ ) -> BarcodeImage:
327
+ return self.generate("datamatrix", text, size=size, module=module, quiet=quiet)
328
+
329
+ def qr(
330
+ self,
331
+ text: str,
332
+ *,
333
+ size: int | None = 512,
334
+ module: int | None = None,
335
+ quiet: int | None = None,
336
+ level: str = "M",
337
+ ) -> BarcodeImage:
338
+ return self.generate(
339
+ "qr", text, size=size, module=module, quiet=quiet, level=level
340
+ )
341
+
342
+ def aztec(
343
+ self,
344
+ text: str,
345
+ *,
346
+ size: int | None = 512,
347
+ module: int | None = None,
348
+ quiet: int | None = None,
349
+ ) -> BarcodeImage:
350
+ return self.generate("aztec", text, size=size, module=module, quiet=quiet)
351
+
352
+ def pdf417(
353
+ self,
354
+ text: str,
355
+ *,
356
+ module: int | None = None,
357
+ quiet: int | None = None,
358
+ level: int | str = 2,
359
+ ) -> BarcodeImage:
360
+ return self.generate("pdf417", text, module=module, quiet=quiet, level=level)
361
+
362
+ def code128(
363
+ self,
364
+ text: str,
365
+ *,
366
+ module: int | None = None,
367
+ height: int | None = 80,
368
+ quiet: int | None = None,
369
+ label: bool = False,
370
+ ) -> BarcodeImage:
371
+ return self.generate(
372
+ "code128", text, module=module, height=height, quiet=quiet, label=label
373
+ )
374
+
375
+ def code39(
376
+ self,
377
+ text: str,
378
+ *,
379
+ module: int | None = None,
380
+ height: int | None = 80,
381
+ quiet: int | None = None,
382
+ label: bool = False,
383
+ fullascii: bool = False,
384
+ ) -> BarcodeImage:
385
+ return self.generate(
386
+ "code39",
387
+ text,
388
+ module=module,
389
+ height=height,
390
+ quiet=quiet,
391
+ label=label,
392
+ fullascii=fullascii,
393
+ )
394
+
395
+ def code93(
396
+ self,
397
+ text: str,
398
+ *,
399
+ module: int | None = None,
400
+ height: int | None = 80,
401
+ quiet: int | None = None,
402
+ label: bool = False,
403
+ fullascii: bool = False,
404
+ ) -> BarcodeImage:
405
+ return self.generate(
406
+ "code93",
407
+ text,
408
+ module=module,
409
+ height=height,
410
+ quiet=quiet,
411
+ label=label,
412
+ fullascii=fullascii,
413
+ )
414
+
415
+ def codabar(
416
+ self,
417
+ text: str,
418
+ *,
419
+ module: int | None = None,
420
+ height: int | None = 80,
421
+ quiet: int | None = None,
422
+ label: bool = False,
423
+ ) -> BarcodeImage:
424
+ return self.generate(
425
+ "codabar", text, module=module, height=height, quiet=quiet, label=label
426
+ )
427
+
428
+ def itf(
429
+ self,
430
+ text: str,
431
+ *,
432
+ module: int | None = None,
433
+ height: int | None = 80,
434
+ quiet: int | None = None,
435
+ label: bool = False,
436
+ ) -> BarcodeImage:
437
+ return self.generate(
438
+ "itf", text, module=module, height=height, quiet=quiet, label=label
439
+ )
440
+
441
+ def code25(
442
+ self,
443
+ text: str,
444
+ *,
445
+ module: int | None = None,
446
+ height: int | None = 80,
447
+ quiet: int | None = None,
448
+ label: bool = False,
449
+ ) -> BarcodeImage:
450
+ return self.generate(
451
+ "code25", text, module=module, height=height, quiet=quiet, label=label
452
+ )
453
+
454
+ def ean13(
455
+ self,
456
+ text: str,
457
+ *,
458
+ module: int | None = None,
459
+ height: int | None = 80,
460
+ quiet: int | None = None,
461
+ label: bool = False,
462
+ ) -> BarcodeImage:
463
+ return self.generate(
464
+ "ean13", text, module=module, height=height, quiet=quiet, label=label
465
+ )
466
+
467
+ def ean8(
468
+ self,
469
+ text: str,
470
+ *,
471
+ module: int | None = None,
472
+ height: int | None = 80,
473
+ quiet: int | None = None,
474
+ label: bool = False,
475
+ ) -> BarcodeImage:
476
+ return self.generate(
477
+ "ean8", text, module=module, height=height, quiet=quiet, label=label
478
+ )
barcodekit/_errors.py ADDED
@@ -0,0 +1,64 @@
1
+ """Public exceptions raised by barcodekit."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shlex
7
+ import subprocess
8
+ from collections.abc import Sequence
9
+
10
+
11
+ def _redact_command(command: Sequence[str]) -> tuple[str, ...]:
12
+ """Return command arguments with the barcode text removed."""
13
+ redacted = list(command)
14
+ for index, argument in enumerate(redacted[:-1]):
15
+ if argument == "--text":
16
+ redacted[index + 1] = "<redacted>"
17
+ return tuple(redacted)
18
+
19
+
20
+ def _display_command(command: Sequence[str]) -> str:
21
+ redacted = _redact_command(command)
22
+ if os.name == "nt":
23
+ return subprocess.list2cmdline(redacted)
24
+ return shlex.join(redacted)
25
+
26
+
27
+ class BarcodeKitError(Exception):
28
+ """Base class for barcodekit errors."""
29
+
30
+
31
+ class BarcodeKitBinaryNotFound(BarcodeKitError):
32
+ """Raised when the configured or bundled executable cannot be found."""
33
+
34
+
35
+ class BarcodeKitUnsupportedPlatform(BarcodeKitError):
36
+ """Raised when barcodekit does not support the current platform."""
37
+
38
+
39
+ class BarcodeKitCommandError(BarcodeKitError):
40
+ """Raised when barcode-rest fails or returns unusable output."""
41
+
42
+ def __init__(
43
+ self,
44
+ returncode: int | None,
45
+ command: Sequence[str],
46
+ stderr: str,
47
+ ) -> None:
48
+ self.returncode = returncode
49
+ self.command = _redact_command(command)
50
+ self.stderr = stderr
51
+ code = "could not be started" if returncode is None else f"exited with code {returncode}"
52
+ detail = stderr.strip() or "no error details"
53
+ super().__init__(f"barcode-rest {code}: {_display_command(command)}; {detail}")
54
+
55
+
56
+ class BarcodeKitTimeout(BarcodeKitError):
57
+ """Raised when barcode-rest does not finish before the timeout."""
58
+
59
+ def __init__(self, timeout: float, command: Sequence[str]) -> None:
60
+ self.timeout = timeout
61
+ self.command = _redact_command(command)
62
+ super().__init__(
63
+ f"barcode-rest timed out after {timeout:g} seconds: {_display_command(command)}"
64
+ )
barcodekit/_version.py ADDED
@@ -0,0 +1,4 @@
1
+ """Package version."""
2
+
3
+ __version__ = "0.1.2"
4
+
barcodekit/py.typed ADDED
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,194 @@
1
+ Metadata-Version: 2.4
2
+ Name: barcodekit
3
+ Version: 0.1.2
4
+ Summary: Generate barcode PNGs with the local barcode-rest command-line tool.
5
+ Project-URL: Homepage, https://github.com/Moge800/barcodekit
6
+ Project-URL: Upstream CLI, https://github.com/Moge800/barcode-rest
7
+ Author: Moge800
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ License-File: THIRD_PARTY_NOTICES.md
11
+ License-File: licenses/barcode-rest-MIT.txt
12
+ License-File: licenses/boombuler-barcode-MIT.txt
13
+ License-File: licenses/go-and-x-image-BSD-3-Clause.txt
14
+ Keywords: barcode,barcode-rest,code128,datamatrix,qr
15
+ Classifier: Development Status :: 3 - Alpha
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: Microsoft :: Windows :: Windows 10
18
+ Classifier: Operating System :: Microsoft :: Windows :: Windows 11
19
+ Classifier: Operating System :: POSIX :: Linux
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3 :: Only
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.10
24
+ Provides-Extra: dev
25
+ Requires-Dist: mypy>=1.14; extra == 'dev'
26
+ Requires-Dist: pytest>=8.0; extra == 'dev'
27
+ Requires-Dist: ruff>=0.9; extra == 'dev'
28
+ Provides-Extra: release
29
+ Requires-Dist: wheel<1,>=0.45; extra == 'release'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # barcodekit
33
+
34
+ [日本語](https://github.com/Moge800/barcodekit/blob/main/README_JP.md)
35
+
36
+ `barcodekit` generates barcode PNG images from Python by invoking the local
37
+ [`barcode-rest`](https://github.com/Moge800/barcode-rest) executable in its
38
+ one-shot CLI mode.
39
+
40
+ Despite the upstream executable's name, **barcodekit does not start the REST
41
+ server and does not use HTTP**. Each operation runs only:
42
+
43
+ ```text
44
+ barcode-rest generate <symbology> --text <text> --output -
45
+ ```
46
+
47
+ PNG bytes are read from standard output and returned directly to Python.
48
+
49
+ ## Quick start
50
+
51
+ Install the platform-specific wheel, then:
52
+
53
+ ```python
54
+ from barcodekit import code128, datamatrix, qr
55
+
56
+ datamatrix("ABC123", size=256).save("dm.png")
57
+ qr("https://example.com", size=512, level="Q").save("qr.png")
58
+ code128("ABC-123456", label=True).save("c128.png")
59
+ ```
60
+
61
+ An explicit engine object provides the same methods:
62
+
63
+ ```python
64
+ from barcodekit import BarcodeKit
65
+
66
+ kit = BarcodeKit(timeout=10)
67
+ image = kit.datamatrix("ABC123", size=256)
68
+
69
+ raw_png = image.to_bytes()
70
+ image.save("dm.png")
71
+ ```
72
+
73
+ For `datamatrix`, `qr`, and `aztec`, `size` and `module` are mutually exclusive.
74
+ Set `size=None` when selecting the module size directly:
75
+
76
+ ```python
77
+ datamatrix("ABC123", size=None, module=8)
78
+ ```
79
+
80
+ ## Executable resolution
81
+
82
+ `barcodekit` resolves the executable when an image is generated, in this order:
83
+
84
+ 1. The file path passed as `BarcodeKit(executable=...)`.
85
+ 2. The file path in `BARCODEKIT_BINARY`.
86
+ 3. The executable bundled in the installed platform wheel.
87
+
88
+ An explicit development binary can be used without building a wheel:
89
+
90
+ ```python
91
+ kit = BarcodeKit(executable=r"C:\tools\barcode-rest.exe")
92
+ kit.datamatrix("ABC123").save("dm.png")
93
+ ```
94
+
95
+ Or with an environment variable:
96
+
97
+ ```powershell
98
+ $env:BARCODEKIT_BINARY = "C:\tools\barcode-rest.exe"
99
+ uv run python example.py
100
+ ```
101
+
102
+ ```bash
103
+ BARCODEKIT_BINARY=/opt/barcode-rest uv run python example.py
104
+ ```
105
+
106
+ These values must be file paths; barcodekit does not search `PATH`.
107
+
108
+ ## Bundled binary wheels
109
+
110
+ Each released wheel is intended to contain exactly one matching
111
+ `barcode-rest` executable. It must not contain a collection of executables for
112
+ other operating systems or CPU architectures.
113
+
114
+ Supported bundled targets:
115
+
116
+ - Windows amd64
117
+ - Linux amd64 using glibc 2.34 or newer, including Ubuntu 22.04 or newer
118
+ - Linux arm64 using glibc, including 64-bit Ubuntu and 64-bit Raspberry Pi OS
119
+
120
+ Unsupported targets:
121
+
122
+ - macOS
123
+ - Windows arm64
124
+ - 32-bit Linux and 32-bit Raspberry Pi OS
125
+ - musl-based Linux distributions such as Alpine Linux
126
+
127
+ Binary-free source distributions are not intended for release. On the
128
+ supported operating systems and CPU architectures listed above, development
129
+ from a source checkout remains supported with `BARCODEKIT_BINARY` or
130
+ `BarcodeKit(executable=...)`.
131
+
132
+ Release builds currently pin
133
+ [`barcode-rest` v0.2.0](https://github.com/Moge800/barcode-rest/releases/tag/v0.2.0).
134
+ The expected SHA-256 values are committed in `checksums/v0.2.0.sha256`.
135
+
136
+ ## Supported symbologies
137
+
138
+ Two-dimensional:
139
+
140
+ - Data Matrix (`datamatrix`)
141
+ - QR Code (`qr`)
142
+ - Aztec (`aztec`)
143
+ - PDF417 (`pdf417`)
144
+
145
+ One-dimensional:
146
+
147
+ - Code 128 (`code128`)
148
+ - Code 39 (`code39`)
149
+ - Code 93 (`code93`)
150
+ - Codabar (`codabar`)
151
+ - Interleaved 2 of 5 (`itf`)
152
+ - Standard 2 of 5 (`code25`)
153
+ - EAN-13 / JAN (`ean13`)
154
+ - EAN-8 (`ean8`)
155
+
156
+ `barcodekit` validates supported options, numeric ranges, text limits, basic
157
+ character sets, and check digits before starting the executable. Encoding
158
+ constraints that depend on the generated symbol remain the responsibility of
159
+ `barcode-rest`.
160
+
161
+ ## Security and privacy
162
+
163
+ - No executable or other data is downloaded at runtime.
164
+ - No server is started.
165
+ - The REST API and HTTP are not used.
166
+ - No outbound network connection is made by the wrapper.
167
+ - Barcode text is passed only to the local `barcode-rest` executable.
168
+ - The wrapper does not log barcode text.
169
+ - Commands shown by wrapper exceptions replace the value after `--text` with
170
+ `<redacted>`. Matching text returned on stderr is also redacted.
171
+
172
+ The text is necessarily passed through the local process command line because
173
+ that is the upstream CLI interface. It may therefore be temporarily visible to
174
+ users or tools with permission to inspect local process arguments.
175
+
176
+ ## Development with uv
177
+
178
+ ```bash
179
+ uv sync --extra dev
180
+ uv run pytest
181
+ uv run ruff check .
182
+ uv run mypy src/barcodekit
183
+ uv build
184
+ ```
185
+
186
+ Unit tests mock `subprocess.run` and do not need the Go executable. If
187
+ `BARCODEKIT_BINARY` is set, the optional integration test generates a real
188
+ Data Matrix image and checks its PNG output.
189
+
190
+ ## License
191
+
192
+ `barcodekit` is MIT licensed. Platform wheels also include the notices and
193
+ license texts listed in
194
+ [THIRD_PARTY_NOTICES.md](https://github.com/Moge800/barcodekit/blob/v0.1.2/THIRD_PARTY_NOTICES.md).
@@ -0,0 +1,16 @@
1
+ barcodekit/__init__.py,sha256=4KLH7Uiauip6QARkFz3jxe4Y4OR4LcRz4NqZgf5wxzI,4409
2
+ barcodekit/_binary.py,sha256=ybYPXUsesaDM3JLbmkfddANsUhDUtMGHCUU0y7l0BJA,2788
3
+ barcodekit/_core.py,sha256=71Isg1W-Aurk1vPSvN0pEQ6qGEndf3WJTSKQZDRCC3Y,15645
4
+ barcodekit/_errors.py,sha256=VzAUUSkhRrTCipUoVeK7Wq6aYplp8Ulj8feAuWMR66g,2082
5
+ barcodekit/_version.py,sha256=e1Iz5EfvRhaCoTklG4I54svyD-fQ1ujsrPto2dSw1cg,51
6
+ barcodekit/py.typed,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
7
+ barcodekit/_bin/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
8
+ barcodekit/_bin/barcode-rest.exe,sha256=Qn-mC-IO7VPBILxsdyZc0s_PjOLaMQR4hcuVikjaWsg,9929216
9
+ barcodekit-0.1.2.dist-info/METADATA,sha256=9KKyLb47foSB3Dj-Pl2SEHL1XW6LrQU5q9pU9jNV4qI,6001
10
+ barcodekit-0.1.2.dist-info/WHEEL,sha256=pp_iTzO0EBcCvlZZV2dJYp6L2tdDWBsn1ceo50wEt8Q,94
11
+ barcodekit-0.1.2.dist-info/licenses/LICENSE,sha256=9BmdgGze2TgObLNWUVMlYMK3n_Q76jWnm_bHkdpBvg4,1087
12
+ barcodekit-0.1.2.dist-info/licenses/THIRD_PARTY_NOTICES.md,sha256=sYLLB_bLSGwDMEcQaMu6_UdTt3C-ggFLt0XELYQE5Sw,691
13
+ barcodekit-0.1.2.dist-info/licenses/licenses/barcode-rest-MIT.txt,sha256=9BmdgGze2TgObLNWUVMlYMK3n_Q76jWnm_bHkdpBvg4,1087
14
+ barcodekit-0.1.2.dist-info/licenses/licenses/boombuler-barcode-MIT.txt,sha256=KxADQwMxUj_V8zUg3vQ1VkxxPLZVNLLYMbpQkhcwUe0,1108
15
+ barcodekit-0.1.2.dist-info/licenses/licenses/go-and-x-image-BSD-3-Clause.txt,sha256=JyHS_WMLrAVQohVdq64G2DWGcuUpCauv_SsstFNktYc,1482
16
+ barcodekit-0.1.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-win_amd64
5
+
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Moge800
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
+
@@ -0,0 +1,15 @@
1
+ # Third-party notices
2
+
3
+ Platform-specific `barcodekit` wheels bundle the `barcode-rest` executable.
4
+ That executable contains the following software:
5
+
6
+ | Component | Version | License |
7
+ | --- | --- | --- |
8
+ | [barcode-rest](https://github.com/Moge800/barcode-rest) | v0.2.0 | MIT |
9
+ | [github.com/boombuler/barcode](https://github.com/boombuler/barcode) | v1.0.2 | MIT |
10
+ | [Go](https://go.dev/) runtime and standard library | 1.26 | BSD-3-Clause |
11
+ | [golang.org/x/image](https://pkg.go.dev/golang.org/x/image) | v0.43.0 | BSD-3-Clause |
12
+
13
+ The corresponding license texts are included in the `licenses` directory.
14
+ `barcodekit` itself is licensed under the MIT License in `LICENSE`.
15
+
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Moge800
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
+
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Florian Sundermann
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
+
@@ -0,0 +1,28 @@
1
+ Copyright 2009 The Go Authors.
2
+
3
+ Redistribution and use in source and binary forms, with or without
4
+ modification, are permitted provided that the following conditions are
5
+ met:
6
+
7
+ * Redistributions of source code must retain the above copyright
8
+ notice, this list of conditions and the following disclaimer.
9
+ * Redistributions in binary form must reproduce the above
10
+ copyright notice, this list of conditions and the following disclaimer
11
+ in the documentation and/or other materials provided with the
12
+ distribution.
13
+ * Neither the name of Google LLC nor the names of its
14
+ contributors may be used to endorse or promote products derived from
15
+ this software without specific prior written permission.
16
+
17
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21
+ OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
+