tapesmith 0.4.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.
- tapesmith/__init__.py +28 -0
- tapesmith/__main__.py +28 -0
- tapesmith/apitokens.py +190 -0
- tapesmith/archive.py +220 -0
- tapesmith/automation/__init__.py +1 -0
- tapesmith/automation/facade.py +131 -0
- tapesmith/automation/hotfolder.py +542 -0
- tapesmith/automation/mqtt.py +470 -0
- tapesmith/automation/telegram.py +506 -0
- tapesmith/automation/update.py +143 -0
- tapesmith/backup.py +239 -0
- tapesmith/calibrate.py +46 -0
- tapesmith/cli.py +446 -0
- tapesmith/cli_cmds/__init__.py +6 -0
- tapesmith/cli_cmds/app.py +34 -0
- tapesmith/cli_cmds/archive.py +85 -0
- tapesmith/cli_cmds/asn.py +117 -0
- tapesmith/cli_cmds/asset.py +205 -0
- tapesmith/cli_cmds/asset_notiz.py +46 -0
- tapesmith/cli_cmds/backup.py +73 -0
- tapesmith/cli_cmds/base.py +439 -0
- tapesmith/cli_cmds/batch.py +219 -0
- tapesmith/cli_cmds/batterie.py +162 -0
- tapesmith/cli_cmds/ble.py +38 -0
- tapesmith/cli_cmds/configcmd.py +110 -0
- tapesmith/cli_cmds/daemon.py +144 -0
- tapesmith/cli_cmds/density.py +126 -0
- tapesmith/cli_cmds/disks.py +170 -0
- tapesmith/cli_cmds/drives.py +73 -0
- tapesmith/cli_cmds/garantie.py +104 -0
- tapesmith/cli_cmds/gui.py +36 -0
- tapesmith/cli_cmds/history.py +66 -0
- tapesmith/cli_cmds/homelab.py +103 -0
- tapesmith/cli_cmds/hotfolder.py +172 -0
- tapesmith/cli_cmds/install.py +55 -0
- tapesmith/cli_cmds/integrate.py +70 -0
- tapesmith/cli_cmds/inv.py +241 -0
- tapesmith/cli_cmds/ka.py +211 -0
- tapesmith/cli_cmds/kabel.py +208 -0
- tapesmith/cli_cmds/kurz.py +119 -0
- tapesmith/cli_cmds/mcp.py +58 -0
- tapesmith/cli_cmds/module.py +72 -0
- tapesmith/cli_cmds/mqtt.py +60 -0
- tapesmith/cli_cmds/nummern.py +79 -0
- tapesmith/cli_cmds/platte.py +142 -0
- tapesmith/cli_cmds/plausi.py +122 -0
- tapesmith/cli_cmds/print_cmd.py +60 -0
- tapesmith/cli_cmds/proxmox.py +129 -0
- tapesmith/cli_cmds/qr.py +123 -0
- tapesmith/cli_cmds/queue.py +135 -0
- tapesmith/cli_cmds/raw.py +112 -0
- tapesmith/cli_cmds/report.py +75 -0
- tapesmith/cli_cmds/reprint.py +57 -0
- tapesmith/cli_cmds/secret.py +73 -0
- tapesmith/cli_cmds/setup.py +72 -0
- tapesmith/cli_cmds/sn_scan.py +88 -0
- tapesmith/cli_cmds/stats.py +73 -0
- tapesmith/cli_cmds/status.py +48 -0
- tapesmith/cli_cmds/tape.py +72 -0
- tapesmith/cli_cmds/telegram.py +72 -0
- tapesmith/cli_cmds/token.py +89 -0
- tapesmith/cli_cmds/tray.py +39 -0
- tapesmith/cli_cmds/uninstall.py +32 -0
- tapesmith/cli_cmds/update.py +129 -0
- tapesmith/cli_cmds/usb.py +32 -0
- tapesmith/cli_cmds/vault.py +214 -0
- tapesmith/clipboard.py +290 -0
- tapesmith/commandreg.py +129 -0
- tapesmith/compat.py +149 -0
- tapesmith/config.py +907 -0
- tapesmith/configcode.py +251 -0
- tapesmith/connection.py +404 -0
- tapesmith/daemon/__init__.py +12 -0
- tapesmith/daemon/__main__.py +5 -0
- tapesmith/daemon/addons.py +219 -0
- tapesmith/daemon/instance.py +272 -0
- tapesmith/daemon/probe.py +200 -0
- tapesmith/daemon/queue.py +420 -0
- tapesmith/daemon/runner.py +290 -0
- tapesmith/daemon/server.py +249 -0
- tapesmith/daemon/service.py +719 -0
- tapesmith/dataimport/__init__.py +2 -0
- tapesmith/dataimport/batch.py +187 -0
- tapesmith/dataimport/mapping.py +114 -0
- tapesmith/dataimport/table.py +161 -0
- tapesmith/density.py +118 -0
- tapesmith/device/__init__.py +0 -0
- tapesmith/device/profile.py +132 -0
- tapesmith/device/profiles/__init__.py +0 -0
- tapesmith/device/profiles/p12.json +22 -0
- tapesmith/doctor.py +97 -0
- tapesmith/document/__init__.py +1 -0
- tapesmith/document/editing.py +335 -0
- tapesmith/document/from_spec.py +105 -0
- tapesmith/document/generators/__init__.py +55 -0
- tapesmith/document/generators/kabelfahne.py +148 -0
- tapesmith/document/generators/kabelwickel.py +82 -0
- tapesmith/document/generators/raster.py +218 -0
- tapesmith/document/generators/raster_export.py +73 -0
- tapesmith/document/model.py +429 -0
- tapesmith/document/render.py +440 -0
- tapesmith/document/snap.py +176 -0
- tapesmith/document/targets.json +16 -0
- tapesmith/document/targets.py +155 -0
- tapesmith/document/undo.py +88 -0
- tapesmith/drives.py +225 -0
- tapesmith/errors.py +279 -0
- tapesmith/export.py +117 -0
- tapesmith/fileutil.py +106 -0
- tapesmith/fonts/DejaVuSans-Bold.ttf +0 -0
- tapesmith/fonts/DejaVuSans.ttf +0 -0
- tapesmith/fonts/DejaVuSansMono.ttf +0 -0
- tapesmith/fonts/LICENSE +187 -0
- tapesmith/fonts/__init__.py +0 -0
- tapesmith/guard.py +207 -0
- tapesmith/gui/__init__.py +7 -0
- tapesmith/gui/__main__.py +7 -0
- tapesmith/gui/hotkey.py +148 -0
- tapesmith/gui/icons.py +130 -0
- tapesmith/gui/preview_model.py +171 -0
- tapesmith/gui/qtutil.py +114 -0
- tapesmith/gui/recent.py +35 -0
- tapesmith/gui/services.py +216 -0
- tapesmith/gui/theme.py +112 -0
- tapesmith/gui/tray.py +994 -0
- tapesmith/history.py +376 -0
- tapesmith/hotkeyspec.py +146 -0
- tapesmith/i18n.py +337 -0
- tapesmith/icons/LICENSE-tabler.txt +21 -0
- tapesmith/icons/__init__.py +1 -0
- tapesmith/icons/app.ico +0 -0
- tapesmith/icons/categories.json +78 -0
- tapesmith/icons/simple/LICENSE-simple-icons.txt +30 -0
- tapesmith/icons/simple/MARKEN.txt +4 -0
- tapesmith/icons/simple/__init__.py +0 -0
- tapesmith/icons/simple/cloudflare.png +0 -0
- tapesmith/icons/simple/debian.png +0 -0
- tapesmith/icons/simple/docker.png +0 -0
- tapesmith/icons/simple/grafana.png +0 -0
- tapesmith/icons/simple/homeassistant.png +0 -0
- tapesmith/icons/simple/index.json +85 -0
- tapesmith/icons/simple/jellyfin.png +0 -0
- tapesmith/icons/simple/linux.png +0 -0
- tapesmith/icons/simple/nextcloud.png +0 -0
- tapesmith/icons/simple/nginx.png +0 -0
- tapesmith/icons/simple/obsidian.png +0 -0
- tapesmith/icons/simple/opnsense.png +0 -0
- tapesmith/icons/simple/paperlessngx.png +0 -0
- tapesmith/icons/simple/pfsense.png +0 -0
- tapesmith/icons/simple/proxmox.png +0 -0
- tapesmith/icons/simple/raspberrypi.png +0 -0
- tapesmith/icons/simple/synology.png +0 -0
- tapesmith/icons/simple/tailscale.png +0 -0
- tapesmith/icons/simple/truenas.png +0 -0
- tapesmith/icons/simple/ubuntu.png +0 -0
- tapesmith/icons/simple/wireguard.png +0 -0
- tapesmith/icons/tabler-icons.ttf +0 -0
- tapesmith/icons/tabler-index.json +1 -0
- tapesmith/imageinput.py +128 -0
- tapesmith/install/__init__.py +6 -0
- tapesmith/install/installer.py +478 -0
- tapesmith/install/junction.py +67 -0
- tapesmith/install/layout.py +201 -0
- tapesmith/install/legacy.py +231 -0
- tapesmith/install/processes.py +221 -0
- tapesmith/install/registry.py +112 -0
- tapesmith/install/shortcuts.py +109 -0
- tapesmith/install/uninstaller.py +213 -0
- tapesmith/install/venv.py +241 -0
- tapesmith/integration.py +429 -0
- tapesmith/integrations/__init__.py +7 -0
- tapesmith/integrations/assetnote.py +78 -0
- tapesmith/integrations/assets.py +327 -0
- tapesmith/integrations/cliprint.py +158 -0
- tapesmith/integrations/codescan.py +290 -0
- tapesmith/integrations/credentials.py +158 -0
- tapesmith/integrations/errors.py +93 -0
- tapesmith/integrations/frontmatter.py +189 -0
- tapesmith/integrations/homeassistant.py +217 -0
- tapesmith/integrations/httpclient.py +91 -0
- tapesmith/integrations/kleinanzeigen.py +300 -0
- tapesmith/integrations/mcpclient.py +186 -0
- tapesmith/integrations/netbox.py +189 -0
- tapesmith/integrations/obsidian.py +325 -0
- tapesmith/integrations/paperless.py +298 -0
- tapesmith/integrations/plausi.py +267 -0
- tapesmith/integrations/proxmox.py +438 -0
- tapesmith/integrations/scancache.py +99 -0
- tapesmith/integrations/settings.py +409 -0
- tapesmith/integrations/shortlink.py +169 -0
- tapesmith/integrations/snippet.py +143 -0
- tapesmith/integrations/tia606.py +218 -0
- tapesmith/integrations/winclip.py +76 -0
- tapesmith/integrations/zfsreplace.py +336 -0
- tapesmith/inventory.py +396 -0
- tapesmith/ipc/__init__.py +1 -0
- tapesmith/ipc/backend.py +650 -0
- tapesmith/ipc/client.py +205 -0
- tapesmith/ipc/codec.py +241 -0
- tapesmith/ipc/launcher.py +66 -0
- tapesmith/ipc/pipe.py +789 -0
- tapesmith/ipc/protocol.py +153 -0
- tapesmith/jobs.py +80 -0
- tapesmith/labelmeta.py +64 -0
- tapesmith/launch.py +93 -0
- tapesmith/locales/__init__.py +0 -0
- tapesmith/locales/de/common.json +18 -0
- tapesmith/locales/de/errors.json +222 -0
- tapesmith/locales/de/modules.json +60 -0
- tapesmith/locales/de/tray.json +114 -0
- tapesmith/locales/en/common.json +18 -0
- tapesmith/locales/en/errors.json +222 -0
- tapesmith/locales/en/modules.json +60 -0
- tapesmith/locales/en/tray.json +114 -0
- tapesmith/locales/messages/en.json +2592 -0
- tapesmith/lock.py +51 -0
- tapesmith/mcpserver/__init__.py +5 -0
- tapesmith/mcpserver/backends.py +196 -0
- tapesmith/mcpserver/server.py +213 -0
- tapesmith/mcpserver/tools.py +266 -0
- tapesmith/migrate.py +117 -0
- tapesmith/modules.py +397 -0
- tapesmith/netinfo.py +65 -0
- tapesmith/numbering.py +215 -0
- tapesmith/pacing.py +8 -0
- tapesmith/paths.py +65 -0
- tapesmith/pipeline.py +404 -0
- tapesmith/printer.py +175 -0
- tapesmith/printhooks.py +37 -0
- tapesmith/protocol/__init__.py +0 -0
- tapesmith/protocol/job.py +77 -0
- tapesmith/protocol/raster.py +28 -0
- tapesmith/protocol/rawcmd.py +85 -0
- tapesmith/protocol/status.py +77 -0
- tapesmith/render/__init__.py +0 -0
- tapesmith/render/barcode.py +93 -0
- tapesmith/render/chain.py +221 -0
- tapesmith/render/checks.py +28 -0
- tapesmith/render/code128.py +100 -0
- tapesmith/render/codes.py +61 -0
- tapesmith/render/compose.py +130 -0
- tapesmith/render/datamatrix.py +54 -0
- tapesmith/render/dither.py +58 -0
- tapesmith/render/fixes.py +191 -0
- tapesmith/render/fonts.py +46 -0
- tapesmith/render/icons.py +309 -0
- tapesmith/render/inkcheck.py +65 -0
- tapesmith/render/qr.py +78 -0
- tapesmith/render/qrcontent.py +175 -0
- tapesmith/render/shapes.py +109 -0
- tapesmith/render/text.py +120 -0
- tapesmith/render/textsize.py +84 -0
- tapesmith/render/zxing.py +83 -0
- tapesmith/reprint.py +150 -0
- tapesmith/secretref.py +149 -0
- tapesmith/selftest.py +522 -0
- tapesmith/setup.py +141 -0
- tapesmith/sourcelimits.py +81 -0
- tapesmith/sshscan.py +360 -0
- tapesmith/stats.py +96 -0
- tapesmith/status.py +122 -0
- tapesmith/statusview.py +169 -0
- tapesmith/support.py +186 -0
- tapesmith/tape/__init__.py +6 -0
- tapesmith/tape/preview.py +50 -0
- tapesmith/tape/profiles.py +179 -0
- tapesmith/tape/rolls.py +187 -0
- tapesmith/tape/suitability.py +69 -0
- tapesmith/tape/tapes.json +22 -0
- tapesmith/templates/__init__.py +0 -0
- tapesmith/templates/builtin/__init__.py +0 -0
- tapesmith/templates/builtin/absender.tapesmith.json +29 -0
- tapesmith/templates/builtin/asn.tapesmith.json +30 -0
- tapesmith/templates/builtin/asset-kurz.tapesmith.json +34 -0
- tapesmith/templates/builtin/asset-tag.tapesmith.json +32 -0
- tapesmith/templates/builtin/aufbewahrungsbox.tapesmith.json +34 -0
- tapesmith/templates/builtin/batterie.tapesmith.json +32 -0
- tapesmith/templates/builtin/datentraeger-qr.tapesmith.json +27 -0
- tapesmith/templates/builtin/datentraeger.tapesmith.json +27 -0
- tapesmith/templates/builtin/eigentum.tapesmith.json +32 -0
- tapesmith/templates/builtin/garantie-qr.tapesmith.json +37 -0
- tapesmith/templates/builtin/garantie.tapesmith.json +31 -0
- tapesmith/templates/builtin/gefriergut.tapesmith.json +43 -0
- tapesmith/templates/builtin/geoeffnet-am.tapesmith.json +28 -0
- tapesmith/templates/builtin/host-ip.tapesmith.json +35 -0
- tapesmith/templates/builtin/ip-label.tapesmith.json +25 -0
- tapesmith/templates/builtin/ka-artikel.tapesmith.json +36 -0
- tapesmith/templates/builtin/kabelfahne.tapesmith.json +37 -0
- tapesmith/templates/builtin/kabelwickel.tapesmith.json +29 -0
- tapesmith/templates/builtin/netzteil-warnung.tapesmith.json +36 -0
- tapesmith/templates/builtin/netzteil-zuordnung.tapesmith.json +31 -0
- tapesmith/templates/builtin/netzteil.tapesmith.json +31 -0
- tapesmith/templates/builtin/ordnerruecken-breit.tapesmith.json +27 -0
- tapesmith/templates/builtin/ordnerruecken-schmal.tapesmith.json +27 -0
- tapesmith/templates/builtin/passthrough.tapesmith.json +31 -0
- tapesmith/templates/builtin/platte-defekt.tapesmith.json +32 -0
- tapesmith/templates/builtin/preis.tapesmith.json +26 -0
- tapesmith/templates/builtin/psu-paar.tapesmith.json +32 -0
- tapesmith/templates/builtin/raster-patchpanel.tapesmith.json +26 -0
- tapesmith/templates/builtin/raster-sicherungskasten.tapesmith.json +25 -0
- tapesmith/templates/builtin/raster-sortiment.tapesmith.json +25 -0
- tapesmith/templates/builtin/raster-switch.tapesmith.json +26 -0
- tapesmith/templates/builtin/reserviert.tapesmith.json +27 -0
- tapesmith/templates/builtin/schule.tapesmith.json +38 -0
- tapesmith/templates/builtin/vm-lxc-qr.tapesmith.json +37 -0
- tapesmith/templates/builtin/vm-lxc.tapesmith.json +29 -0
- tapesmith/templates/builtin/vorratsdose.tapesmith.json +37 -0
- tapesmith/templates/builtin/wartung.tapesmith.json +35 -0
- tapesmith/templates/fill.py +183 -0
- tapesmith/templates/gallery.py +239 -0
- tapesmith/templates/lint.py +191 -0
- tapesmith/templates/model.py +849 -0
- tapesmith/templates/render.py +245 -0
- tapesmith/templates/serial.py +54 -0
- tapesmith/templates/series.py +130 -0
- tapesmith/templates/store.py +110 -0
- tapesmith/transport/__init__.py +0 -0
- tapesmith/transport/base.py +121 -0
- tapesmith/transport/ble.py +239 -0
- tapesmith/transport/btports.py +70 -0
- tapesmith/transport/resolve.py +50 -0
- tapesmith/transport/serial_port.py +72 -0
- tapesmith/transport/usb.py +289 -0
- tapesmith/update/__init__.py +10 -0
- tapesmith/update/apply.py +310 -0
- tapesmith/update/errors.py +23 -0
- tapesmith/update/manifest.py +278 -0
- tapesmith/update/service.py +394 -0
- tapesmith/update/signing.py +93 -0
- tapesmith/update/sources.py +308 -0
- tapesmith/update/state.py +67 -0
- tapesmith/update/trusted_keys.json +8 -0
- tapesmith/verify.py +136 -0
- tapesmith/webapi/__init__.py +5 -0
- tapesmith/webapi/accent.py +26 -0
- tapesmith/webapi/access.py +144 -0
- tapesmith/webapi/actions.py +115 -0
- tapesmith/webapi/app.py +124 -0
- tapesmith/webapi/batchapi.py +150 -0
- tapesmith/webapi/context.py +125 -0
- tapesmith/webapi/convert.py +51 -0
- tapesmith/webapi/datajson.py +95 -0
- tapesmith/webapi/documents.py +93 -0
- tapesmith/webapi/drafts.py +226 -0
- tapesmith/webapi/errors.py +197 -0
- tapesmith/webapi/events.py +105 -0
- tapesmith/webapi/homelab_common.py +61 -0
- tapesmith/webapi/homelab_routers.py +30 -0
- tapesmith/webapi/labels.py +575 -0
- tapesmith/webapi/language.py +52 -0
- tapesmith/webapi/mcp_http.py +65 -0
- tapesmith/webapi/previews.py +70 -0
- tapesmith/webapi/printing.py +110 -0
- tapesmith/webapi/ratelimit.py +93 -0
- tapesmith/webapi/routes_access.py +210 -0
- tapesmith/webapi/routes_assets.py +213 -0
- tapesmith/webapi/routes_codescan.py +58 -0
- tapesmith/webapi/routes_core.py +148 -0
- tapesmith/webapi/routes_data.py +516 -0
- tapesmith/webapi/routes_drafts.py +74 -0
- tapesmith/webapi/routes_editor.py +396 -0
- tapesmith/webapi/routes_family.py +270 -0
- tapesmith/webapi/routes_ha.py +106 -0
- tapesmith/webapi/routes_homelab.py +69 -0
- tapesmith/webapi/routes_kabel.py +218 -0
- tapesmith/webapi/routes_kleinanzeigen.py +145 -0
- tapesmith/webapi/routes_labels.py +74 -0
- tapesmith/webapi/routes_modules.py +78 -0
- tapesmith/webapi/routes_paperless.py +116 -0
- tapesmith/webapi/routes_plausi.py +133 -0
- tapesmith/webapi/routes_proxmox.py +93 -0
- tapesmith/webapi/routes_settings.py +327 -0
- tapesmith/webapi/routes_short.py +203 -0
- tapesmith/webapi/routes_support.py +46 -0
- tapesmith/webapi/routes_system.py +179 -0
- tapesmith/webapi/routes_templates.py +474 -0
- tapesmith/webapi/routes_update.py +154 -0
- tapesmith/webapi/routes_vault.py +202 -0
- tapesmith/webapi/routes_zfs.py +103 -0
- tapesmith/webapi/security.py +254 -0
- tapesmith/webapi/server.py +229 -0
- tapesmith/webapi/session.py +70 -0
- tapesmith/webapi/settings_schema.py +462 -0
- tapesmith/webui/__init__.py +8 -0
- tapesmith/webui/browser.py +261 -0
- tapesmith/webui/static/assets/DataList-BNdLqY0h.js +1 -0
- tapesmith/webui/static/assets/FamilyApp-ArkuANKE.js +1 -0
- tapesmith/webui/static/assets/ListToolbar-C4dAZi00.js +1 -0
- tapesmith/webui/static/assets/ModuleSettingsButton-ZUbj_1ik.js +1 -0
- tapesmith/webui/static/assets/NavList-Dwey-1PS.js +1 -0
- tapesmith/webui/static/assets/PrintOptionsBar-gdugjiWW.js +1 -0
- tapesmith/webui/static/assets/RowActions-CG90mya5.js +1 -0
- tapesmith/webui/static/assets/Section-B0h_MINH.js +1 -0
- tapesmith/webui/static/assets/TableScroll-BWJfFpya.js +1 -0
- tapesmith/webui/static/assets/TapePreview-DKSbEPDH.js +1 -0
- tapesmith/webui/static/assets/TextSizeSelect-NmOXQvqm.js +1 -0
- tapesmith/webui/static/assets/WarningList-TFhCJHVc.js +1 -0
- tapesmith/webui/static/assets/api-DAWnziWm.js +1 -0
- tapesmith/webui/static/assets/fluent-DAQMrKDT.js +194 -0
- tapesmith/webui/static/assets/gateMessages-BjG-W2n9.js +8 -0
- tapesmith/webui/static/assets/index-B0ibC1fA.js +5 -0
- tapesmith/webui/static/assets/index-B3ieSe8A.js +1 -0
- tapesmith/webui/static/assets/index-B6LmWkwg.js +1 -0
- tapesmith/webui/static/assets/index-BBGAZEVC.js +1 -0
- tapesmith/webui/static/assets/index-BFOUDlKz.js +1 -0
- tapesmith/webui/static/assets/index-BG0glZqe.js +2 -0
- tapesmith/webui/static/assets/index-Bk_o53YF.js +2 -0
- tapesmith/webui/static/assets/index-Bm_YhjSS.js +39 -0
- tapesmith/webui/static/assets/index-C1culFZj.js +2 -0
- tapesmith/webui/static/assets/index-C7z4MrlV.js +1 -0
- tapesmith/webui/static/assets/index-C8Wfr4XK.js +1 -0
- tapesmith/webui/static/assets/index-CSBHxRZo.js +4 -0
- tapesmith/webui/static/assets/index-CUM2lQP7.js +7 -0
- tapesmith/webui/static/assets/index-CUc32c9J.js +1 -0
- tapesmith/webui/static/assets/index-CYOf3IW7.js +1 -0
- tapesmith/webui/static/assets/index-CqdKpquX.js +1 -0
- tapesmith/webui/static/assets/index-DRDqtjoV.js +2 -0
- tapesmith/webui/static/assets/index-DXW3DZbF.js +1 -0
- tapesmith/webui/static/assets/index-NbuszyXs.js +3 -0
- tapesmith/webui/static/assets/index-QLKmO2qo.js +1 -0
- tapesmith/webui/static/assets/index-TA-i_7q_.js +6 -0
- tapesmith/webui/static/assets/index-qLvSlZoG.js +1 -0
- tapesmith/webui/static/assets/index-sdE0n6Yf.js +1 -0
- tapesmith/webui/static/assets/index-vkhP0yaY.js +2 -0
- tapesmith/webui/static/assets/konva-CZCmb_o2.js +51 -0
- tapesmith/webui/static/assets/numberText-CaqySO_R.js +1 -0
- tapesmith/webui/static/assets/systemTitle-qe0pdYpR.js +1 -0
- tapesmith/webui/static/assets/time-Ctx9DKCc.js +1 -0
- tapesmith/webui/static/assets/unsaved-Dbna7DRF.js +1 -0
- tapesmith/webui/static/index.html +19 -0
- tapesmith-0.4.0.dist-info/METADATA +246 -0
- tapesmith-0.4.0.dist-info/RECORD +436 -0
- tapesmith-0.4.0.dist-info/WHEEL +4 -0
- tapesmith-0.4.0.dist-info/entry_points.txt +6 -0
- tapesmith-0.4.0.dist-info/licenses/LICENSE +21 -0
- tapesmith-0.4.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +86 -0
tapesmith/__init__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Tapesmith: label studio for the Phomemo P12."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
__version__ = "0.4.0"
|
|
6
|
+
|
|
7
|
+
# Bis 0.2.x hieß die App "P12 Label" und las Umgebungsvariablen mit dem Präfix P12LABEL_.
|
|
8
|
+
# Alte Namen gelten weiter als Rückfall: ist TAPESMITH_X nicht gesetzt, übernimmt es den Wert
|
|
9
|
+
# von P12LABEL_X. Das geschieht einmal beim Import des Pakets, also vor jedem Lesezugriff.
|
|
10
|
+
LEGACY_ENV_PREFIX = "P12LABEL_"
|
|
11
|
+
ENV_PREFIX = "TAPESMITH_"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def apply_legacy_env(environ=None) -> list[str]:
|
|
15
|
+
"""Kopiert gesetzte `P12LABEL_*`-Variablen nach `TAPESMITH_*`, sofern dort nichts steht.
|
|
16
|
+
Gibt die übernommenen neuen Namen zurück."""
|
|
17
|
+
env = os.environ if environ is None else environ
|
|
18
|
+
taken = []
|
|
19
|
+
for key in list(env):
|
|
20
|
+
if key.upper().startswith(LEGACY_ENV_PREFIX):
|
|
21
|
+
new = ENV_PREFIX + key[len(LEGACY_ENV_PREFIX):].upper()
|
|
22
|
+
if new not in env:
|
|
23
|
+
env[new] = env[key]
|
|
24
|
+
taken.append(new)
|
|
25
|
+
return taken
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
apply_legacy_env()
|
tapesmith/__main__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""`python -m tapesmith …`: Einstieg ohne eigene EXE.
|
|
2
|
+
|
|
3
|
+
`install` und `uninstall` gehen direkt an Installer bzw. Deinstallation (ohne den restlichen
|
|
4
|
+
CLI-Unterbau zu laden, so läuft die Deinstallation auch unter `pythonw.exe` ohne Konsole); alles
|
|
5
|
+
andere ist die normale Kommandozeile (`tapesmith.cli`)."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main(argv: list[str] | None = None) -> int:
|
|
13
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
14
|
+
if args and args[0] == "install":
|
|
15
|
+
from tapesmith.install import installer
|
|
16
|
+
|
|
17
|
+
return installer.main(args[1:])
|
|
18
|
+
if args and args[0] == "uninstall":
|
|
19
|
+
from tapesmith.install import uninstaller
|
|
20
|
+
|
|
21
|
+
return uninstaller.main(args[1:])
|
|
22
|
+
from tapesmith import cli
|
|
23
|
+
|
|
24
|
+
return cli.main(args)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if __name__ == "__main__":
|
|
28
|
+
sys.exit(main())
|
tapesmith/apitokens.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""API-Token-Speicher: nur Hash auf der Platte, Klartext genau einmal beim Anlegen.
|
|
2
|
+
|
|
3
|
+
Die Datei (`access/tokens.json`) wird bei geänderter mtime neu gelesen, damit eine Änderung per
|
|
4
|
+
CLI sofort im laufenden Druckdienst wirkt (mehrere `TokenStore`-Instanzen auf derselben Datei).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import hmac
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import re
|
|
14
|
+
import secrets
|
|
15
|
+
import threading
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from tapesmith import fileutil, paths
|
|
21
|
+
from tapesmith.i18n import N_, _t
|
|
22
|
+
|
|
23
|
+
ROLES = ("admin", "drucken", "familie")
|
|
24
|
+
ROLE_LABELS = {"admin": N_("Verwaltung"), "drucken": N_("Drucken"), "familie": N_("Familie")}
|
|
25
|
+
|
|
26
|
+
_NAME_RE = re.compile(r"^[\w .-]{1,40}$")
|
|
27
|
+
_SECRET_FORM_RE = re.compile(r"^p12_([0-9a-f]{8})_[A-Za-z0-9_-]{20,}$")
|
|
28
|
+
|
|
29
|
+
log = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class TokenInfo:
|
|
34
|
+
id: str
|
|
35
|
+
name: str
|
|
36
|
+
role: str
|
|
37
|
+
created: str
|
|
38
|
+
last_used: str | None
|
|
39
|
+
|
|
40
|
+
def to_json(self) -> dict:
|
|
41
|
+
return {
|
|
42
|
+
"id": self.id,
|
|
43
|
+
"name": self.name,
|
|
44
|
+
"role": self.role,
|
|
45
|
+
"role_label": _t(ROLE_LABELS[self.role]),
|
|
46
|
+
"created": self.created,
|
|
47
|
+
"last_used": self.last_used,
|
|
48
|
+
"hint": f"p12_{self.id}_…",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _entry_to_info(entry: dict) -> TokenInfo:
|
|
53
|
+
return TokenInfo(entry["id"], entry["name"], entry["role"], entry["created"],
|
|
54
|
+
entry.get("last_used"))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TokenStore:
|
|
58
|
+
def __init__(self, path: Path | None = None, *, now=datetime.now,
|
|
59
|
+
touch_interval_s: float = 600.0) -> None:
|
|
60
|
+
self.path = Path(path) if path is not None else paths.app_dir() / "access" / "tokens.json"
|
|
61
|
+
self._now = now
|
|
62
|
+
self._touch_interval_s = touch_interval_s
|
|
63
|
+
self._lock = threading.Lock()
|
|
64
|
+
self._mtime: float | None = None
|
|
65
|
+
self._tokens: list[dict] = []
|
|
66
|
+
|
|
67
|
+
# ---------- Laden/Speichern ----------
|
|
68
|
+
|
|
69
|
+
def _load_if_changed(self) -> None:
|
|
70
|
+
try:
|
|
71
|
+
mtime = self.path.stat().st_mtime
|
|
72
|
+
except FileNotFoundError:
|
|
73
|
+
self._mtime = None
|
|
74
|
+
self._tokens = []
|
|
75
|
+
return
|
|
76
|
+
if mtime == self._mtime:
|
|
77
|
+
return
|
|
78
|
+
try:
|
|
79
|
+
data = json.loads(self.path.read_text(encoding="utf-8"))
|
|
80
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
81
|
+
raise ValueError(_t("Token-Datei {path} ist beschädigt: {exc}", path=self.path, exc=exc)) from exc
|
|
82
|
+
self._tokens = list(data.get("tokens", []))
|
|
83
|
+
self._mtime = mtime
|
|
84
|
+
|
|
85
|
+
def _save(self) -> None:
|
|
86
|
+
data = {"version": 1, "tokens": self._tokens}
|
|
87
|
+
fileutil.atomic_write_text(self.path, json.dumps(data, indent=2, ensure_ascii=False))
|
|
88
|
+
self._mtime = self.path.stat().st_mtime
|
|
89
|
+
|
|
90
|
+
def _find_entry(self, id_or_name: str) -> dict | None:
|
|
91
|
+
for entry in self._tokens:
|
|
92
|
+
if entry["id"] == id_or_name:
|
|
93
|
+
return entry
|
|
94
|
+
matches = [e for e in self._tokens if e["name"].casefold() == id_or_name.casefold()]
|
|
95
|
+
if len(matches) > 1:
|
|
96
|
+
raise ValueError(_t("Mehrere Tokens mit dem Namen '{id_or_name}'", id_or_name=id_or_name))
|
|
97
|
+
return matches[0] if matches else None
|
|
98
|
+
|
|
99
|
+
# ---------- öffentliche API ----------
|
|
100
|
+
|
|
101
|
+
def create(self, name: str, role: str) -> tuple[TokenInfo, str]:
|
|
102
|
+
if role not in ROLES:
|
|
103
|
+
raise ValueError(_t("Rolle '{role}' unbekannt: erlaubt {items}", role=role, items=', '.join(ROLES)))
|
|
104
|
+
if not isinstance(name, str) or not _NAME_RE.match(name):
|
|
105
|
+
raise ValueError(
|
|
106
|
+
_t("Token-Name {name!r} ungültig: 1..40 Zeichen (Buchstaben, Ziffern, Leerzeichen, ._-)", name=name))
|
|
107
|
+
with self._lock:
|
|
108
|
+
self._load_if_changed()
|
|
109
|
+
for entry in self._tokens:
|
|
110
|
+
if entry["name"].casefold() == name.casefold():
|
|
111
|
+
raise ValueError(_t("Token-Name '{name}' ist schon vergeben", name=name))
|
|
112
|
+
token_id = secrets.token_hex(4)
|
|
113
|
+
plaintext = f"p12_{token_id}_{secrets.token_urlsafe(32)}"
|
|
114
|
+
created = self._now().isoformat()
|
|
115
|
+
entry = {
|
|
116
|
+
"id": token_id,
|
|
117
|
+
"name": name,
|
|
118
|
+
"role": role,
|
|
119
|
+
"hash": "sha256:" + hashlib.sha256(plaintext.encode("utf-8")).hexdigest(),
|
|
120
|
+
"created": created,
|
|
121
|
+
"last_used": None,
|
|
122
|
+
}
|
|
123
|
+
self._tokens.append(entry)
|
|
124
|
+
self._save()
|
|
125
|
+
return _entry_to_info(entry), plaintext
|
|
126
|
+
|
|
127
|
+
def list(self) -> list[TokenInfo]:
|
|
128
|
+
with self._lock:
|
|
129
|
+
self._load_if_changed()
|
|
130
|
+
entries = sorted(self._tokens, key=lambda e: e["created"])
|
|
131
|
+
return [_entry_to_info(e) for e in entries]
|
|
132
|
+
|
|
133
|
+
def find(self, id_or_name: str) -> TokenInfo | None:
|
|
134
|
+
with self._lock:
|
|
135
|
+
self._load_if_changed()
|
|
136
|
+
entry = self._find_entry(id_or_name)
|
|
137
|
+
return _entry_to_info(entry) if entry is not None else None
|
|
138
|
+
|
|
139
|
+
def revoke(self, id_or_name: str) -> TokenInfo:
|
|
140
|
+
with self._lock:
|
|
141
|
+
self._load_if_changed()
|
|
142
|
+
entry = self._find_entry(id_or_name)
|
|
143
|
+
if entry is None:
|
|
144
|
+
raise KeyError(id_or_name)
|
|
145
|
+
self._tokens.remove(entry)
|
|
146
|
+
self._save()
|
|
147
|
+
return _entry_to_info(entry)
|
|
148
|
+
|
|
149
|
+
def verify(self, secret: str) -> TokenInfo | None:
|
|
150
|
+
if not isinstance(secret, str) or not secret:
|
|
151
|
+
return None
|
|
152
|
+
match = _SECRET_FORM_RE.match(secret)
|
|
153
|
+
if not match:
|
|
154
|
+
return None
|
|
155
|
+
token_id = match.group(1)
|
|
156
|
+
with self._lock:
|
|
157
|
+
try:
|
|
158
|
+
self._load_if_changed()
|
|
159
|
+
except ValueError as exc:
|
|
160
|
+
log.warning("Token-Datei nicht lesbar, Dienst läuft weiter: %s", exc)
|
|
161
|
+
return None
|
|
162
|
+
entry = None
|
|
163
|
+
for candidate in self._tokens:
|
|
164
|
+
if candidate["id"] == token_id:
|
|
165
|
+
entry = candidate
|
|
166
|
+
break
|
|
167
|
+
if entry is None:
|
|
168
|
+
return None
|
|
169
|
+
candidate_hash = "sha256:" + hashlib.sha256(secret.encode("utf-8")).hexdigest()
|
|
170
|
+
if not hmac.compare_digest(candidate_hash, entry["hash"]):
|
|
171
|
+
return None
|
|
172
|
+
self._touch(entry)
|
|
173
|
+
return _entry_to_info(entry)
|
|
174
|
+
|
|
175
|
+
def _touch(self, entry: dict) -> None:
|
|
176
|
+
"""Schreibt `last_used` höchstens alle `touch_interval_s`; Schreibfehler werden ignoriert."""
|
|
177
|
+
now = self._now()
|
|
178
|
+
last_used = entry.get("last_used")
|
|
179
|
+
if last_used:
|
|
180
|
+
try:
|
|
181
|
+
elapsed = (now - datetime.fromisoformat(last_used)).total_seconds()
|
|
182
|
+
except ValueError:
|
|
183
|
+
elapsed = self._touch_interval_s
|
|
184
|
+
if elapsed < self._touch_interval_s:
|
|
185
|
+
return
|
|
186
|
+
entry["last_used"] = now.isoformat()
|
|
187
|
+
try:
|
|
188
|
+
self._save()
|
|
189
|
+
except OSError:
|
|
190
|
+
pass
|
tapesmith/archive.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""Git-Archiv der Drucke: jeder erfolgreiche Druck wird (wenn `archive.dir` gesetzt
|
|
2
|
+
ist) als JSON plus 1-Bit-PNG unter `<archive.dir>\\JJJJ\\` abgelegt. Sensible Drucke bekommen
|
|
3
|
+
kein PNG und im JSON werden verdächtige Werte zusätzlich maskiert. Ein optionaler Git-Commit
|
|
4
|
+
läuft nur nach bestandenem Secret-Scan, niemals `push`.
|
|
5
|
+
|
|
6
|
+
`archive_hook` wird vom Druckdienst und vom Direktdruck nach jedem Druck aufgerufen;
|
|
7
|
+
ein Fehler beim Archivieren darf den Druck selbst nie scheitern lassen.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import re
|
|
14
|
+
import subprocess
|
|
15
|
+
from collections.abc import Callable, Sequence
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from io import BytesIO
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from PIL import Image
|
|
22
|
+
|
|
23
|
+
from tapesmith import __version__
|
|
24
|
+
from tapesmith.device.profile import DeviceProfile, load_profile
|
|
25
|
+
from tapesmith.export import head_to_landscape
|
|
26
|
+
from tapesmith.fileutil import atomic_write_bytes, atomic_write_text
|
|
27
|
+
from tapesmith.history import HistoryEntry, HistoryStore
|
|
28
|
+
from tapesmith.templates.fill import REDACTED
|
|
29
|
+
from tapesmith.i18n import N_, _t
|
|
30
|
+
|
|
31
|
+
ArchiveHook = Callable[[Any], list[str]] # Any = pipeline.PrintOutcome
|
|
32
|
+
GitRunner = Callable[[list[str], Path], tuple[int, str, str]]
|
|
33
|
+
|
|
34
|
+
_UMLAUT = {"ä": "ae", "ö": "oe", "ü": "ue", "ß": "ss", "Ä": "ae", "Ö": "oe", "Ü": "ue"}
|
|
35
|
+
_SLUG_MAX = 40
|
|
36
|
+
_NON_SLUG = re.compile(r"[^a-z0-9]+")
|
|
37
|
+
_SECRET_KEY = re.compile(r"(?i)(password|passwort|pw|pin|token|secret|key)")
|
|
38
|
+
_ARCHIVE_EXTS = ("*.json", "*.txt", "*.md", "*.csv")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class SecretFinding:
|
|
43
|
+
kind: str # z. B. "privater Schlüssel", "Passwort/Token-Zuweisung", "WLAN-Passwort", …
|
|
44
|
+
excerpt: str # maskiert: erste 4 Zeichen + "…"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
SECRET_PATTERNS: tuple[tuple[str, re.Pattern], ...] = (
|
|
48
|
+
(N_("privater Schlüssel"), re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----")),
|
|
49
|
+
(N_("Passwort/Token-Zuweisung"), re.compile(
|
|
50
|
+
r'(?i)\b(?:password|passwort|kennwort|pass|pwd|token|secret|api[_-]?key)\b'
|
|
51
|
+
r'["\']?\s*[:=]\s*["\']?(?!•)([^\s"\',;]{4,})')),
|
|
52
|
+
(N_("WLAN-Passwort"), re.compile(r"WIFI:[^\n]*?P:(?!;)([^;]+);")),
|
|
53
|
+
("GitHub-Token", re.compile(r"gh[pousr]_[A-Za-z0-9]{30,}")),
|
|
54
|
+
(N_("AWS-Schlüssel"), re.compile(r"AKIA[0-9A-Z]{16}")),
|
|
55
|
+
("JWT", re.compile(r"eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.")),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _mask(value: str) -> str:
|
|
60
|
+
return f"{value[:4]}…"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def scan_secrets(text: str) -> list[SecretFinding]:
|
|
64
|
+
"""Alle verdächtigen Muster in `text`; REDACTED-Werte (`templates.fill.REDACTED`) zählen
|
|
65
|
+
nicht (die Muster schließen sie per negativem Lookahead aus)."""
|
|
66
|
+
findings: list[SecretFinding] = []
|
|
67
|
+
for kind, pattern in SECRET_PATTERNS:
|
|
68
|
+
for match in pattern.finditer(text):
|
|
69
|
+
value = match.group(1) if match.groups() else match.group(0)
|
|
70
|
+
findings.append(SecretFinding(kind=_t(kind), excerpt=_mask(value)))
|
|
71
|
+
return findings
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def scan_path(path: Path) -> list[tuple[Path, SecretFinding]]:
|
|
75
|
+
"""Secret-Scan über eine einzelne Datei oder rekursiv über `*.json`/`*.txt`/`*.md`/`*.csv`
|
|
76
|
+
eines Ordners. Unlesbare Dateien werden übersprungen, nie eine Ausnahme."""
|
|
77
|
+
path = Path(path)
|
|
78
|
+
if path.is_file():
|
|
79
|
+
files = [path]
|
|
80
|
+
else:
|
|
81
|
+
files = sorted({f for pattern in _ARCHIVE_EXTS for f in path.rglob(pattern)})
|
|
82
|
+
results: list[tuple[Path, SecretFinding]] = []
|
|
83
|
+
for file in files:
|
|
84
|
+
try:
|
|
85
|
+
text = file.read_text(encoding="utf-8")
|
|
86
|
+
except (OSError, UnicodeDecodeError):
|
|
87
|
+
continue
|
|
88
|
+
for finding in scan_secrets(text):
|
|
89
|
+
results.append((file, finding))
|
|
90
|
+
return results
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _slug(text: str) -> str:
|
|
94
|
+
for umlaut, ascii_ in _UMLAUT.items():
|
|
95
|
+
text = text.replace(umlaut, ascii_)
|
|
96
|
+
text = _NON_SLUG.sub("-", text.lower()).strip("-")
|
|
97
|
+
return (text or "label")[:_SLUG_MAX].strip("-") or "label"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def entry_record(entry: HistoryEntry) -> dict:
|
|
101
|
+
"""JSON-Datensatz für das Archiv: bei sensiblen Einträgen wird jeder Wert, dessen
|
|
102
|
+
Schlüsselname nach einem Geheimnis aussieht und der nicht schon REDACTED ist, zusätzlich
|
|
103
|
+
maskiert (der Verlauf maskiert Vorlagenwerte bereits; das hier ist ein zweites Netz für
|
|
104
|
+
freie Werte wie Zwischenablage-Text). Der Titel bleibt unverändert."""
|
|
105
|
+
values = dict(entry.values)
|
|
106
|
+
if entry.sensitive:
|
|
107
|
+
values = {k: (REDACTED if (_SECRET_KEY.search(k) and v != REDACTED) else v)
|
|
108
|
+
for k, v in values.items()}
|
|
109
|
+
return {
|
|
110
|
+
"id": entry.id,
|
|
111
|
+
"created": entry.created.isoformat(),
|
|
112
|
+
"source": entry.source,
|
|
113
|
+
"kind": entry.kind,
|
|
114
|
+
"title": entry.title,
|
|
115
|
+
"template": entry.template,
|
|
116
|
+
"values": values,
|
|
117
|
+
"length_mm": entry.length_mm,
|
|
118
|
+
"tape_mm": entry.tape_mm,
|
|
119
|
+
"copies": entry.copies,
|
|
120
|
+
"chained": entry.chained,
|
|
121
|
+
"status": entry.status,
|
|
122
|
+
"sensitive": entry.sensitive,
|
|
123
|
+
"app_version": __version__,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _save_png(path: Path, image: Image.Image) -> None:
|
|
128
|
+
buf = BytesIO()
|
|
129
|
+
image.save(buf, "PNG")
|
|
130
|
+
atomic_write_bytes(path, buf.getvalue())
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def archive_entry(entry: HistoryEntry, head: Image.Image | None, directory: Path,
|
|
134
|
+
profile: DeviceProfile) -> list[Path]:
|
|
135
|
+
"""Legt einen Verlaufseintrag unter `directory/JJJJ/` ab: immer die JSON, dazu ein
|
|
136
|
+
1-Bit-Querformat-PNG, wenn ein Kopfbild vorliegt und der Eintrag nicht sensibel ist."""
|
|
137
|
+
year_dir = Path(directory) / f"{entry.created.year}"
|
|
138
|
+
base = f"{entry.created:%Y-%m-%d_%H%M%S}_{entry.id:05d}_{_slug(entry.title)}"
|
|
139
|
+
json_path = year_dir / f"{base}.json"
|
|
140
|
+
atomic_write_text(json_path, json.dumps(entry_record(entry), indent=2, ensure_ascii=False, sort_keys=True))
|
|
141
|
+
paths = [json_path]
|
|
142
|
+
if head is not None and not entry.sensitive:
|
|
143
|
+
png_path = year_dir / f"{base}.png"
|
|
144
|
+
_save_png(png_path, head_to_landscape(head, profile).convert("1"))
|
|
145
|
+
paths.append(png_path)
|
|
146
|
+
return paths
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _default_git_runner(argv: list[str], cwd: Path) -> tuple[int, str, str]:
|
|
150
|
+
result = subprocess.run(argv, cwd=str(cwd), capture_output=True, text=True)
|
|
151
|
+
return result.returncode, result.stdout, result.stderr
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def git_commit(files: Sequence[Path], directory: Path, message: str, *,
|
|
155
|
+
runner: GitRunner | None = None) -> str | None:
|
|
156
|
+
"""Committet `files` (relativ zu `directory`), niemals `push`. Vorher ein Secret-Scan über
|
|
157
|
+
alle `.json`-Dateien: ein Treffer verhindert den Commit. Rückgabe `None` bei Erfolg, sonst
|
|
158
|
+
ein deutscher Hinweis."""
|
|
159
|
+
directory = Path(directory)
|
|
160
|
+
run = runner if runner is not None else _default_git_runner
|
|
161
|
+
|
|
162
|
+
for file in files:
|
|
163
|
+
file = Path(file)
|
|
164
|
+
if file.suffix != ".json":
|
|
165
|
+
continue
|
|
166
|
+
try:
|
|
167
|
+
text = file.read_text(encoding="utf-8")
|
|
168
|
+
except OSError:
|
|
169
|
+
continue
|
|
170
|
+
findings = scan_secrets(text)
|
|
171
|
+
if findings:
|
|
172
|
+
return _t("Nicht committet: {kind} in {file}", kind=findings[0].kind, file=file)
|
|
173
|
+
|
|
174
|
+
rc, _out, _err = run(["git", "rev-parse", "--is-inside-work-tree"], directory)
|
|
175
|
+
if rc != 0:
|
|
176
|
+
return _t("Kein Git-Repo: {directory}", directory=directory)
|
|
177
|
+
|
|
178
|
+
rel = [Path(f).relative_to(directory).as_posix() for f in files]
|
|
179
|
+
rc, out, err = run(["git", "add", "--", *rel], directory)
|
|
180
|
+
if rc != 0:
|
|
181
|
+
return _t("Git-Commit fehlgeschlagen: {strip}", strip=(err or out).strip())
|
|
182
|
+
rc, out, err = run(["git", "commit", "-m", message, "--", *rel], directory)
|
|
183
|
+
if rc != 0:
|
|
184
|
+
return _t("Git-Commit fehlgeschlagen: {strip}", strip=(err or out).strip())
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def archive_hook(cfg: dict, *, history_factory: Callable[[], HistoryStore] = HistoryStore,
|
|
189
|
+
profile_loader: Callable[[], DeviceProfile] | None = None,
|
|
190
|
+
git_runner: GitRunner | None = None) -> ArchiveHook | None:
|
|
191
|
+
"""Baut den Archiv-Hook aus der Konfiguration; `None`, wenn `archive.dir` fehlt (Default wie
|
|
192
|
+
in `SECTION_DEFAULTS`: `{"dir": None, "git_commit": False}`). Der Hook fängt jede Ausnahme,
|
|
193
|
+
denn ein Druck darf nie am Archiv scheitern."""
|
|
194
|
+
archive_cfg = cfg.get("archive") or {}
|
|
195
|
+
directory = archive_cfg.get("dir")
|
|
196
|
+
if not directory:
|
|
197
|
+
return None
|
|
198
|
+
directory = Path(directory)
|
|
199
|
+
do_commit = bool(archive_cfg.get("git_commit", False))
|
|
200
|
+
|
|
201
|
+
def hook(outcome) -> list[str]:
|
|
202
|
+
if outcome.status != "ok" or outcome.history_id is None:
|
|
203
|
+
return []
|
|
204
|
+
try:
|
|
205
|
+
with history_factory() as store:
|
|
206
|
+
entry = store.get(outcome.history_id)
|
|
207
|
+
head = store.head_image(outcome.history_id)
|
|
208
|
+
profile = profile_loader() if profile_loader is not None else load_profile()
|
|
209
|
+
files = archive_entry(entry, head, directory, profile)
|
|
210
|
+
notes: list[str] = []
|
|
211
|
+
if do_commit:
|
|
212
|
+
message = f"Label #{entry.id}: {entry.title}"
|
|
213
|
+
result = git_commit(files, directory, message, runner=git_runner)
|
|
214
|
+
if result is not None:
|
|
215
|
+
notes.append(result)
|
|
216
|
+
return notes
|
|
217
|
+
except Exception as exc: # noqa: BLE001 (ein Druck darf nie am Archiv scheitern)
|
|
218
|
+
return [_t("Archiv: {exc}", exc=exc)]
|
|
219
|
+
|
|
220
|
+
return hook
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Automatisierung im Druckdienst: Fassade für Addons und Kanäle (Hotfolder, MQTT, Telegram, MCP)."""
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Fassade `LabelFacade`: ein schmaler, stabiler Zugang zum Druckdienst für Addons.
|
|
2
|
+
|
|
3
|
+
Hotfolder, MQTT, Telegram und MCP drucken nur über diese Klasse. Sie nutzt denselben Weg wie die
|
|
4
|
+
Web-API (`webapi.labels`), damit Vorschau = Druck gilt und Fehldruckschutz, Kontingente, Verlauf,
|
|
5
|
+
Warteschlange und Restmeter überall gleich wirken. Jede Druck- und Vorschau-Methode verlangt die
|
|
6
|
+
Quelle des Auftrags (`origin`, einer aus `jobs.SOURCES`).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import base64
|
|
12
|
+
import re
|
|
13
|
+
from collections.abc import Callable, Sequence
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from tapesmith import config as config_mod
|
|
17
|
+
from tapesmith.ipc import codec, pipe
|
|
18
|
+
from tapesmith.jobs import SOURCES
|
|
19
|
+
from tapesmith.templates import store
|
|
20
|
+
from tapesmith.templates.model import TemplateError
|
|
21
|
+
from tapesmith.webapi import convert, datajson, labels
|
|
22
|
+
from tapesmith.webapi.context import ApiContext
|
|
23
|
+
from tapesmith.webapi.events import EventBroker
|
|
24
|
+
from tapesmith.webapi.labels import LabelNotPrintable
|
|
25
|
+
from tapesmith.webapi.printing import options_from_json
|
|
26
|
+
from tapesmith.webapi.routes_templates import template_summary_json
|
|
27
|
+
from tapesmith.i18n import _t
|
|
28
|
+
|
|
29
|
+
__all__ = ["LabelFacade", "LabelNotPrintable"]
|
|
30
|
+
|
|
31
|
+
# Nur reine Vorlagennamen (wie `routes_templates._find_or_404`): nie Pfade laden.
|
|
32
|
+
_NAME_RE = re.compile(r"^[A-Za-z0-9._-]{1,64}$")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _check_origin(origin: str) -> str:
|
|
36
|
+
if origin not in SOURCES:
|
|
37
|
+
raise ValueError(_t("Unbekannte Quelle '{origin}' (erlaubt: {items})", origin=origin, items=', '.join(SOURCES)))
|
|
38
|
+
return origin
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class LabelFacade:
|
|
42
|
+
"""Zugang der Addons zum Druckdienst (drucken, Vorschau, Status, Warteschlange, Verlauf)."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, service, *, ctx: ApiContext | None = None):
|
|
45
|
+
if ctx is None:
|
|
46
|
+
ctx = ApiContext(service=service, home_key=pipe.home_key(), token="", broker=EventBroker(),
|
|
47
|
+
static_dir=Path("."), port=0)
|
|
48
|
+
self.service = service
|
|
49
|
+
self.ctx = ctx
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def from_ctx(cls, ctx: ApiContext) -> LabelFacade:
|
|
53
|
+
return cls(ctx.service, ctx=ctx)
|
|
54
|
+
|
|
55
|
+
# ---------- Konfiguration ----------
|
|
56
|
+
|
|
57
|
+
def config(self) -> dict:
|
|
58
|
+
"""Aktuelle Konfiguration (bei jedem Aufruf frisch)."""
|
|
59
|
+
return self.ctx.config()
|
|
60
|
+
|
|
61
|
+
# ---------- Labels ----------
|
|
62
|
+
|
|
63
|
+
def render(self, source: dict, options: dict | None = None, *, origin: str) -> dict:
|
|
64
|
+
"""`RenderJson` (Vorschau nur bei druckbaren Labels)."""
|
|
65
|
+
_check_origin(origin)
|
|
66
|
+
opts = options_from_json(options)
|
|
67
|
+
return labels.render_json(self.ctx, labels.resolve(self.ctx, source, origin=origin), opts)
|
|
68
|
+
|
|
69
|
+
def print(self, source: dict, options: dict | None = None, *, origin: str) -> dict:
|
|
70
|
+
"""`OutcomeJson`; `LabelNotPrintable` (ValueError), wenn die Quelle nicht druckbar ist."""
|
|
71
|
+
_check_origin(origin)
|
|
72
|
+
return labels.print_source(self.ctx, source, options_from_json(options), origin=origin)
|
|
73
|
+
|
|
74
|
+
def preview_png(self, source: dict, options: dict | None = None, *, origin: str,
|
|
75
|
+
raster: bool = False) -> bytes:
|
|
76
|
+
"""PNG der Vorschau (`design_png`, mit `raster=True` das 1-Bit-Rasterbild)."""
|
|
77
|
+
_check_origin(origin)
|
|
78
|
+
opts = options_from_json(options)
|
|
79
|
+
resolved = labels.resolve(self.ctx, source, origin=origin)
|
|
80
|
+
out = labels.render_json(self.ctx, resolved, opts)
|
|
81
|
+
preview = out.get("preview")
|
|
82
|
+
if not out.get("ok") or not preview:
|
|
83
|
+
raise LabelNotPrintable(resolved)
|
|
84
|
+
return base64.b64decode(preview["raster_png" if raster else "design_png"])
|
|
85
|
+
|
|
86
|
+
def template_summaries(self, names: Sequence[str] | None = None) -> list[dict]:
|
|
87
|
+
"""`TemplateSummary` aller Vorlagen oder der genannten (in dieser Reihenfolge, Unbekannte fehlen)."""
|
|
88
|
+
favs = set(config_mod.gui_setting(self.config(), "favorites"))
|
|
89
|
+
if names is None:
|
|
90
|
+
return [template_summary_json(t, favs) for t in store.list_templates()]
|
|
91
|
+
out: list[dict] = []
|
|
92
|
+
for name in names:
|
|
93
|
+
if not isinstance(name, str) or not _NAME_RE.match(name):
|
|
94
|
+
continue
|
|
95
|
+
try:
|
|
96
|
+
template = store.find_template(name)
|
|
97
|
+
except TemplateError:
|
|
98
|
+
continue
|
|
99
|
+
out.append(template_summary_json(template, favs))
|
|
100
|
+
return out
|
|
101
|
+
|
|
102
|
+
# ---------- Zustand ----------
|
|
103
|
+
|
|
104
|
+
def status(self) -> dict:
|
|
105
|
+
"""`StatusJson` aus dem Cache des Dienstes (kein Druckerkontakt)."""
|
|
106
|
+
report = self.service.status(fresh=False)
|
|
107
|
+
return convert.status_json(report, now=self.ctx.now(), mac=self.config().get("mac"))
|
|
108
|
+
|
|
109
|
+
def state(self) -> dict:
|
|
110
|
+
return codec.encode_state(self.service.state())
|
|
111
|
+
|
|
112
|
+
def queue(self, include_done: bool = False) -> dict:
|
|
113
|
+
return self.service.queue_snapshot(include_done=include_done)
|
|
114
|
+
|
|
115
|
+
def cancel_queued(self, job_id: int) -> bool:
|
|
116
|
+
return bool(self.service.queue_cancel(job_id))
|
|
117
|
+
|
|
118
|
+
def history(self, limit: int = 20, query: str = "") -> list[dict]:
|
|
119
|
+
history = self.service.history
|
|
120
|
+
return [datajson.history_entry_json(e, history) for e in history.search(query, limit)]
|
|
121
|
+
|
|
122
|
+
def remaining_m(self) -> float | None:
|
|
123
|
+
remaining = self.service.rolls.remaining_mm()
|
|
124
|
+
return None if remaining is None else remaining / 1000
|
|
125
|
+
|
|
126
|
+
def verified(self) -> tuple[str, ...]:
|
|
127
|
+
return tuple(self.ctx.profile().verified)
|
|
128
|
+
|
|
129
|
+
def subscribe(self, fn: Callable[[str, dict], None]) -> None:
|
|
130
|
+
"""Ereignisse des Dienstes (`job`, `status`, `queue` …) an `fn(event, data)`."""
|
|
131
|
+
self.service.add_emitter(fn)
|