lr-lumenlabel-cli 3.0.0__py3-none-any.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.
@@ -0,0 +1,121 @@
1
+ Metadata-Version: 2.4
2
+ Name: lr-lumenlabel-cli
3
+ Version: 3.0.0
4
+ Summary: CLI for the LumenRadio LumenLabel label printing station: import label packages fetched from Arena with gladiator, list labels and inspect the station, locally, on the LAN or through the Fleet relay.
5
+ Author-email: Jonas Estberger <jonas.estberger@lumenradio.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://gitlab.com/lumenradio/production/lumenlabel
8
+ Project-URL: Changelog, https://gitlab.com/lumenradio/production/lumenlabel/-/blob/main/CHANGELOG.md
9
+ Keywords: LumenRadio,LumenLabel,label printing,Arena,CLI
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Environment :: Console
14
+ Requires-Python: >=3.12
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: typer>=0.12
18
+ Requires-Dist: rich>=13.7
19
+ Requires-Dist: httpx>=0.27
20
+ Requires-Dist: lr-fleet>=0.7
21
+ Provides-Extra: dev
22
+ Requires-Dist: build>=1.2.1; extra == "dev"
23
+ Requires-Dist: twine>=6.1; extra == "dev"
24
+ Requires-Dist: pytest>=8.4; extra == "dev"
25
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
26
+ Requires-Dist: black>=25.9; extra == "dev"
27
+ Requires-Dist: setuptools_scm>=8; extra == "dev"
28
+ Provides-Extra: package
29
+ Requires-Dist: pyinstaller==6.21.0; extra == "package"
30
+ Dynamic: license-file
31
+
32
+ # lr-lumenlabel-cli
33
+
34
+ The `lumenlabel` command for LumenRadio LumenLabel label printing stations.
35
+ It turns a label fetched from Arena with gladiator into a label package and
36
+ imports it into a station, locally, on the LAN or through the Fleet relay.
37
+
38
+ ## Installation
39
+
40
+ ```bash
41
+ pip install lr-lumenlabel-cli
42
+ ```
43
+
44
+ The LumenLabel installers also put it on every station: the `.msi` as
45
+ `lumenlabel.exe` on the system `PATH`, the `.deb` as `/usr/bin/lumenlabel`.
46
+ There it imports from a USB stick without a network (airgapped mode).
47
+
48
+ ## Quick start
49
+
50
+ ```bash
51
+ # Fetch the label with its Arena metadata and BOM children
52
+ gladiator get-files 109-1182 --recursive --meta-data --out 109-1182
53
+
54
+ # Write the manifest and show Build Words and media warnings, offline
55
+ lumenlabel label import 109-1182 --dry-run
56
+
57
+ # Import into the LumenLabel running on this computer
58
+ lumenlabel label import 109-1182
59
+
60
+ # Or remember a station once, then import into it
61
+ lumenlabel login --station https://fleet.cloud.lumenradio.com/v1/s/<uid>
62
+ lumenlabel label import 109-1182
63
+ lumenlabel label list
64
+ ```
65
+
66
+ ## Commands
67
+
68
+ ```
69
+ lumenlabel [--station URL] [--token TOKEN] COMMAND # global options first
70
+
71
+ lumenlabel login --station URL [--token TOKEN]
72
+ # Checks the station and remembers it, with its token, in
73
+ # ~/.config/lumenlabel/login.json (0600). Every later command then talks
74
+ # to that station as if it were the local one. --base-url is an alias of
75
+ # --station, as in lumentest.
76
+ lumenlabel logout
77
+
78
+ lumenlabel label import DIR [--dry-run] [--invalidate] [--output rich|json]
79
+ lumenlabel label list [--output rich|json]
80
+ lumenlabel label remove ARTICLE [--yes]
81
+
82
+ lumenlabel station [--output rich|json] # info
83
+ lumenlabel station history [--limit N] [--before ID]
84
+ lumenlabel station logs [--tail N]
85
+
86
+ lumenlabel api METHOD PATH [--data JSON|@FILE] # any /api/ endpoint
87
+ lumenlabel --version
88
+ ```
89
+
90
+ ## Which station
91
+
92
+ 1. `--station URL` (alias `--base-url`), or `LUMENLABEL_STATION` /
93
+ `LUMENLABEL_URL`.
94
+ 2. The station saved by `lumenlabel login`.
95
+ 3. The LumenLabel on this computer, `http://127.0.0.1:<port>`. The port is
96
+ read from the station's `lumenlabel.json` (default 17081).
97
+
98
+ | URL | Authentication |
99
+ |---|---|
100
+ | `https://<fleet>/v1/s/<uid>` (Fleet relay) | Your Fleet sign-in (`fleet login`; the first command opens the browser if needed) |
101
+ | `http://127.0.0.1:<port>` (on the station) | None |
102
+ | `http://<station>:<port>` (LAN, `instrumentation.host` `0.0.0.0` on the station) | The station token, shown in admin mode on the station |
103
+
104
+ ## `label import`
105
+
106
+ - The manifest (`artifact-manifest.json`) is built from the `.arena.json`
107
+ sidecars gladiator writes with `--meta-data`. A folder with a manifest and no
108
+ sidecars is imported as it is.
109
+ - Private attachments (NiceLabel `.nlbl` sources), web links and BOM child
110
+ folders are left out. Only the declared files are uploaded.
111
+ - The media in `configuration.json` (`media_article`, `media_name`) is checked
112
+ against the BOM's 111 item. A mismatch is a warning; fix it with a new
113
+ revision in Arena. The CLI never changes `configuration.json`.
114
+ - A read-only folder (a write-protected USB stick) is imported through a
115
+ temporary copy.
116
+ - An interrupted import resumes: run it again and only the files that did not
117
+ arrive are sent. `--invalidate` discards what the station has staged.
118
+ - The output shows the revision, the Build Words, the media and any warnings.
119
+
120
+ Specification: `doc/label-package.md` and `doc/api-reference.md` in the
121
+ [LumenLabel repository](https://gitlab.com/lumenradio/production/lumenlabel).
@@ -0,0 +1,25 @@
1
+ lr_lumenlabel_cli-3.0.0.dist-info/licenses/LICENSE,sha256=cEnG3HeMPa3GKtuENkytOQaNJS9rwMMU_KWHObdWbOY,1070
2
+ lumenlabel/__init__.py,sha256=fj1drjftWKEMssQcz1DSbwsgdSPDNARj-L2vVZ9WFxc,233
3
+ lumenlabel/__main__.py,sha256=J0EGKk19CsNu3cRu60xavdV_0XGE3eIcSG5ELFJnj4U,60
4
+ lumenlabel/_version.py,sha256=Q5KDuzxagP6DhS0ZWCmUUKtPbVMNBgYpVtHVHkQdyPQ,520
5
+ lumenlabel/build_words.py,sha256=GcBtfcxUpFNWKeHBFt-is_KfItmzOaxFMgVw3Hewdds,2639
6
+ lumenlabel/cli.py,sha256=3xw7OmX13MdbRXb2zklgckmDuQrJA_9HKCnweBC0bao,1547
7
+ lumenlabel/client.py,sha256=Urf3csuoACv88p1bkh8AlXRTSh06YRxwWNZMAXerIoo,12654
8
+ lumenlabel/config.py,sha256=aViVRNq4z3mOrTXt2qzjUd3QSSe3g1fOpSAt9tC3DCc,4344
9
+ lumenlabel/config_rules.py,sha256=vbeQyAMAQrQq8jJsSFldheayUKOGWopw_wmAuHyFDkA,3593
10
+ lumenlabel/importer.py,sha256=SL5DaED9BcCTtkgdor5u7S76lYFVmIFejcplkqmBG3U,1755
11
+ lumenlabel/manifest_gen.py,sha256=vo6dO_Rx_m_DvgAF9rMBMFrADNhok8tmpIYByq2Xvgs,8683
12
+ lumenlabel/media_check.py,sha256=AACA8pbOmuu1jCwJcyUNGOeIb93g7_7tu7Bx4DLpGRE,2534
13
+ lumenlabel/package.py,sha256=65KIamKMW-hecW-QU0u2Lwo2TPfpuOyw676JPObuQng,10330
14
+ lumenlabel/shared.py,sha256=5OGLGoH1MN_ZsiQqaA37LUpzOBP64DdSmoenxOVTS4w,4056
15
+ lumenlabel/commands/__init__.py,sha256=yH4or3_GDcDktoZVW8xswsjNEgmy4JJu3oKhvIfM7Jw,44
16
+ lumenlabel/commands/api.py,sha256=7vWAWLUxDu57xZnm5iLPX73oSNRqDZPC6d9DBManb2Q,1934
17
+ lumenlabel/commands/auth.py,sha256=ryg1gxLHw1Bwnv3bqFSWf7iJvF71eoLRXb3qyOYBP-4,1763
18
+ lumenlabel/commands/label.py,sha256=jIiVCCHice5DaxjFa1bpsimQ7qcAklyB-Nl9bi7CTS8,8404
19
+ lumenlabel/commands/station.py,sha256=g-0mzHs6zZAJcfK-b5MAnf-Wa_fahZANR2Ocn_eyJeQ,3829
20
+ lumenlabel/wordlist/bip39-english.txt,sha256=L17tU6Rye0v4iA2PPxme_JDlhQNkbZ_47_Oi7Tsk29o,13116
21
+ lr_lumenlabel_cli-3.0.0.dist-info/METADATA,sha256=hrsjqls1XkeKXb-FmDvMCQihaczt3SQL82JrTQzDcQg,4908
22
+ lr_lumenlabel_cli-3.0.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
23
+ lr_lumenlabel_cli-3.0.0.dist-info/entry_points.txt,sha256=CUcNRGGZTs_AOOIfeQT4s9I_sG9cGm-QGu8acga9roo,50
24
+ lr_lumenlabel_cli-3.0.0.dist-info/top_level.txt,sha256=xW-gRTlQyGA7rjRy7FaqXLuItrMeY9LL9k6koSbxAZQ,11
25
+ lr_lumenlabel_cli-3.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ lumenlabel = lumenlabel.cli:app
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 LumenRadio AB
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.
@@ -0,0 +1 @@
1
+ lumenlabel
lumenlabel/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """lr-lumenlabel-cli: the ``lumenlabel`` command for LumenLabel stations."""
2
+
3
+ try:
4
+ from lumenlabel._version import __version__
5
+ except ImportError: # pragma: no cover - written by setuptools_scm
6
+ __version__ = "0.0.0+unknown"
lumenlabel/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from lumenlabel.cli import app
2
+
3
+ app(prog_name="lumenlabel")
lumenlabel/_version.py ADDED
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '3.0.0'
22
+ __version_tuple__ = version_tuple = (3, 0, 0)
23
+
24
+ __commit_id__ = commit_id = None
@@ -0,0 +1,75 @@
1
+ """Build Words: a two-word BIP39 fingerprint of a label package's files[].
2
+
3
+ A copy of the station's ``src/build_words.py`` and ``src/wordlist/``, so the
4
+ CLI and the station always give the same words for the same files.
5
+ ``cli/tests/test_build_words.py`` fails when the copies drift.
6
+ Algorithm: doc/label-package.md §6.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import json
13
+ from pathlib import Path
14
+ from typing import Any, Iterable
15
+
16
+ BUILD_WORDS_COUNT = 2
17
+ _BITS_PER_WORD = 11 # 2048-word list
18
+ _TOTAL_BITS = BUILD_WORDS_COUNT * _BITS_PER_WORD # 22
19
+
20
+ # The fields of LumenTest's ManifestFile with their defaults. LumenLabel's
21
+ # ManifestFile has no ``version`` and no ``os``; they hash as null and "any".
22
+ _FIELD_DEFAULTS: dict[str, Any] = {
23
+ "path": None,
24
+ "role": None,
25
+ "title": None,
26
+ "version": None,
27
+ "sha256": None,
28
+ "os": "any",
29
+ }
30
+
31
+ WORDLIST_PATH = Path(__file__).resolve().parent / "wordlist" / "bip39-english.txt"
32
+
33
+
34
+ def _load_wordlist(path: Path) -> tuple[str, ...]:
35
+ words = tuple(path.read_text(encoding="utf-8").split())
36
+ if len(words) != 2048:
37
+ raise RuntimeError(f"expected 2048-word BIP39 wordlist, got {len(words)}")
38
+ return words
39
+
40
+
41
+ _WORDLIST: tuple[str, ...] = _load_wordlist(WORDLIST_PATH)
42
+
43
+ __all__ = ["BUILD_WORDS_COUNT", "content_digest", "build_words"]
44
+
45
+
46
+ def _canonical_entry(entry: Any) -> dict[str, Any]:
47
+ """Reduces a ManifestFile model or a files[] dict to the canonical fields."""
48
+ if hasattr(entry, "model_dump"):
49
+ entry = entry.model_dump(mode="json")
50
+ return {key: entry.get(key, default) for key, default in _FIELD_DEFAULTS.items()}
51
+
52
+
53
+ def content_digest(files: Iterable[Any]) -> str:
54
+ """SHA-256 hex digest over the canonical ``files[]`` entries, sorted by path."""
55
+ entries = sorted((_canonical_entry(f) for f in files), key=lambda f: f["path"])
56
+ digest = hashlib.sha256()
57
+ for obj in entries:
58
+ blob = json.dumps(
59
+ obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False
60
+ ).encode("utf-8")
61
+ # JSON escapes newlines inside strings, so "\n" separates unambiguously.
62
+ digest.update(blob + b"\n")
63
+ return digest.hexdigest()
64
+
65
+
66
+ def words_from_digest(digest: str) -> str:
67
+ """The two words for a :func:`content_digest` value."""
68
+ head = bytes.fromhex(digest)[:3]
69
+ value = int.from_bytes(head, "big") >> (8 * len(head) - _TOTAL_BITS)
70
+ return f"{_WORDLIST[value >> _BITS_PER_WORD]} {_WORDLIST[value & 0x7FF]}"
71
+
72
+
73
+ def build_words(files: Iterable[Any]) -> str:
74
+ """Two BIP39 words for *files*. The manifest header is not hashed."""
75
+ return words_from_digest(content_digest(files))
lumenlabel/cli.py ADDED
@@ -0,0 +1,60 @@
1
+ """The ``lumenlabel`` command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from . import __version__
8
+ from .commands.api import api
9
+ from .commands.auth import login, logout
10
+ from .commands.label import label_app
11
+ from .commands.station import station_app
12
+ from .shared import GlobalOptions
13
+
14
+ app = typer.Typer(
15
+ help=(
16
+ "Import labels into a LumenLabel station and inspect it, locally, on the "
17
+ "LAN or through the Fleet relay."
18
+ ),
19
+ no_args_is_help=True,
20
+ )
21
+ app.command("login")(login)
22
+ app.command("logout")(logout)
23
+ app.add_typer(label_app, name="label")
24
+ app.add_typer(station_app, name="station")
25
+ app.command("api")(api)
26
+
27
+
28
+ def _version(value: bool) -> None:
29
+ if value:
30
+ typer.echo(__version__)
31
+ raise typer.Exit()
32
+
33
+
34
+ @app.callback()
35
+ def main(
36
+ ctx: typer.Context,
37
+ station: str = typer.Option(
38
+ None,
39
+ "--station",
40
+ "--base-url",
41
+ "-s",
42
+ envvar=["LUMENLABEL_STATION", "LUMENLABEL_URL"],
43
+ help="Station URL for this command (--base-url as in lumentest). "
44
+ "Default: the remembered station, else the local station API.",
45
+ ),
46
+ token: str = typer.Option(
47
+ None,
48
+ "--token",
49
+ envvar="LUMENLABEL_TOKEN",
50
+ help="Station token for a LAN URL. Default: the remembered one.",
51
+ ),
52
+ version: bool = typer.Option(
53
+ False,
54
+ "--version",
55
+ callback=_version,
56
+ is_eager=True,
57
+ help="Show the version and exit.",
58
+ ),
59
+ ) -> None:
60
+ ctx.obj = GlobalOptions(station=station, token=token)