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.
Files changed (436) hide show
  1. tapesmith/__init__.py +28 -0
  2. tapesmith/__main__.py +28 -0
  3. tapesmith/apitokens.py +190 -0
  4. tapesmith/archive.py +220 -0
  5. tapesmith/automation/__init__.py +1 -0
  6. tapesmith/automation/facade.py +131 -0
  7. tapesmith/automation/hotfolder.py +542 -0
  8. tapesmith/automation/mqtt.py +470 -0
  9. tapesmith/automation/telegram.py +506 -0
  10. tapesmith/automation/update.py +143 -0
  11. tapesmith/backup.py +239 -0
  12. tapesmith/calibrate.py +46 -0
  13. tapesmith/cli.py +446 -0
  14. tapesmith/cli_cmds/__init__.py +6 -0
  15. tapesmith/cli_cmds/app.py +34 -0
  16. tapesmith/cli_cmds/archive.py +85 -0
  17. tapesmith/cli_cmds/asn.py +117 -0
  18. tapesmith/cli_cmds/asset.py +205 -0
  19. tapesmith/cli_cmds/asset_notiz.py +46 -0
  20. tapesmith/cli_cmds/backup.py +73 -0
  21. tapesmith/cli_cmds/base.py +439 -0
  22. tapesmith/cli_cmds/batch.py +219 -0
  23. tapesmith/cli_cmds/batterie.py +162 -0
  24. tapesmith/cli_cmds/ble.py +38 -0
  25. tapesmith/cli_cmds/configcmd.py +110 -0
  26. tapesmith/cli_cmds/daemon.py +144 -0
  27. tapesmith/cli_cmds/density.py +126 -0
  28. tapesmith/cli_cmds/disks.py +170 -0
  29. tapesmith/cli_cmds/drives.py +73 -0
  30. tapesmith/cli_cmds/garantie.py +104 -0
  31. tapesmith/cli_cmds/gui.py +36 -0
  32. tapesmith/cli_cmds/history.py +66 -0
  33. tapesmith/cli_cmds/homelab.py +103 -0
  34. tapesmith/cli_cmds/hotfolder.py +172 -0
  35. tapesmith/cli_cmds/install.py +55 -0
  36. tapesmith/cli_cmds/integrate.py +70 -0
  37. tapesmith/cli_cmds/inv.py +241 -0
  38. tapesmith/cli_cmds/ka.py +211 -0
  39. tapesmith/cli_cmds/kabel.py +208 -0
  40. tapesmith/cli_cmds/kurz.py +119 -0
  41. tapesmith/cli_cmds/mcp.py +58 -0
  42. tapesmith/cli_cmds/module.py +72 -0
  43. tapesmith/cli_cmds/mqtt.py +60 -0
  44. tapesmith/cli_cmds/nummern.py +79 -0
  45. tapesmith/cli_cmds/platte.py +142 -0
  46. tapesmith/cli_cmds/plausi.py +122 -0
  47. tapesmith/cli_cmds/print_cmd.py +60 -0
  48. tapesmith/cli_cmds/proxmox.py +129 -0
  49. tapesmith/cli_cmds/qr.py +123 -0
  50. tapesmith/cli_cmds/queue.py +135 -0
  51. tapesmith/cli_cmds/raw.py +112 -0
  52. tapesmith/cli_cmds/report.py +75 -0
  53. tapesmith/cli_cmds/reprint.py +57 -0
  54. tapesmith/cli_cmds/secret.py +73 -0
  55. tapesmith/cli_cmds/setup.py +72 -0
  56. tapesmith/cli_cmds/sn_scan.py +88 -0
  57. tapesmith/cli_cmds/stats.py +73 -0
  58. tapesmith/cli_cmds/status.py +48 -0
  59. tapesmith/cli_cmds/tape.py +72 -0
  60. tapesmith/cli_cmds/telegram.py +72 -0
  61. tapesmith/cli_cmds/token.py +89 -0
  62. tapesmith/cli_cmds/tray.py +39 -0
  63. tapesmith/cli_cmds/uninstall.py +32 -0
  64. tapesmith/cli_cmds/update.py +129 -0
  65. tapesmith/cli_cmds/usb.py +32 -0
  66. tapesmith/cli_cmds/vault.py +214 -0
  67. tapesmith/clipboard.py +290 -0
  68. tapesmith/commandreg.py +129 -0
  69. tapesmith/compat.py +149 -0
  70. tapesmith/config.py +907 -0
  71. tapesmith/configcode.py +251 -0
  72. tapesmith/connection.py +404 -0
  73. tapesmith/daemon/__init__.py +12 -0
  74. tapesmith/daemon/__main__.py +5 -0
  75. tapesmith/daemon/addons.py +219 -0
  76. tapesmith/daemon/instance.py +272 -0
  77. tapesmith/daemon/probe.py +200 -0
  78. tapesmith/daemon/queue.py +420 -0
  79. tapesmith/daemon/runner.py +290 -0
  80. tapesmith/daemon/server.py +249 -0
  81. tapesmith/daemon/service.py +719 -0
  82. tapesmith/dataimport/__init__.py +2 -0
  83. tapesmith/dataimport/batch.py +187 -0
  84. tapesmith/dataimport/mapping.py +114 -0
  85. tapesmith/dataimport/table.py +161 -0
  86. tapesmith/density.py +118 -0
  87. tapesmith/device/__init__.py +0 -0
  88. tapesmith/device/profile.py +132 -0
  89. tapesmith/device/profiles/__init__.py +0 -0
  90. tapesmith/device/profiles/p12.json +22 -0
  91. tapesmith/doctor.py +97 -0
  92. tapesmith/document/__init__.py +1 -0
  93. tapesmith/document/editing.py +335 -0
  94. tapesmith/document/from_spec.py +105 -0
  95. tapesmith/document/generators/__init__.py +55 -0
  96. tapesmith/document/generators/kabelfahne.py +148 -0
  97. tapesmith/document/generators/kabelwickel.py +82 -0
  98. tapesmith/document/generators/raster.py +218 -0
  99. tapesmith/document/generators/raster_export.py +73 -0
  100. tapesmith/document/model.py +429 -0
  101. tapesmith/document/render.py +440 -0
  102. tapesmith/document/snap.py +176 -0
  103. tapesmith/document/targets.json +16 -0
  104. tapesmith/document/targets.py +155 -0
  105. tapesmith/document/undo.py +88 -0
  106. tapesmith/drives.py +225 -0
  107. tapesmith/errors.py +279 -0
  108. tapesmith/export.py +117 -0
  109. tapesmith/fileutil.py +106 -0
  110. tapesmith/fonts/DejaVuSans-Bold.ttf +0 -0
  111. tapesmith/fonts/DejaVuSans.ttf +0 -0
  112. tapesmith/fonts/DejaVuSansMono.ttf +0 -0
  113. tapesmith/fonts/LICENSE +187 -0
  114. tapesmith/fonts/__init__.py +0 -0
  115. tapesmith/guard.py +207 -0
  116. tapesmith/gui/__init__.py +7 -0
  117. tapesmith/gui/__main__.py +7 -0
  118. tapesmith/gui/hotkey.py +148 -0
  119. tapesmith/gui/icons.py +130 -0
  120. tapesmith/gui/preview_model.py +171 -0
  121. tapesmith/gui/qtutil.py +114 -0
  122. tapesmith/gui/recent.py +35 -0
  123. tapesmith/gui/services.py +216 -0
  124. tapesmith/gui/theme.py +112 -0
  125. tapesmith/gui/tray.py +994 -0
  126. tapesmith/history.py +376 -0
  127. tapesmith/hotkeyspec.py +146 -0
  128. tapesmith/i18n.py +337 -0
  129. tapesmith/icons/LICENSE-tabler.txt +21 -0
  130. tapesmith/icons/__init__.py +1 -0
  131. tapesmith/icons/app.ico +0 -0
  132. tapesmith/icons/categories.json +78 -0
  133. tapesmith/icons/simple/LICENSE-simple-icons.txt +30 -0
  134. tapesmith/icons/simple/MARKEN.txt +4 -0
  135. tapesmith/icons/simple/__init__.py +0 -0
  136. tapesmith/icons/simple/cloudflare.png +0 -0
  137. tapesmith/icons/simple/debian.png +0 -0
  138. tapesmith/icons/simple/docker.png +0 -0
  139. tapesmith/icons/simple/grafana.png +0 -0
  140. tapesmith/icons/simple/homeassistant.png +0 -0
  141. tapesmith/icons/simple/index.json +85 -0
  142. tapesmith/icons/simple/jellyfin.png +0 -0
  143. tapesmith/icons/simple/linux.png +0 -0
  144. tapesmith/icons/simple/nextcloud.png +0 -0
  145. tapesmith/icons/simple/nginx.png +0 -0
  146. tapesmith/icons/simple/obsidian.png +0 -0
  147. tapesmith/icons/simple/opnsense.png +0 -0
  148. tapesmith/icons/simple/paperlessngx.png +0 -0
  149. tapesmith/icons/simple/pfsense.png +0 -0
  150. tapesmith/icons/simple/proxmox.png +0 -0
  151. tapesmith/icons/simple/raspberrypi.png +0 -0
  152. tapesmith/icons/simple/synology.png +0 -0
  153. tapesmith/icons/simple/tailscale.png +0 -0
  154. tapesmith/icons/simple/truenas.png +0 -0
  155. tapesmith/icons/simple/ubuntu.png +0 -0
  156. tapesmith/icons/simple/wireguard.png +0 -0
  157. tapesmith/icons/tabler-icons.ttf +0 -0
  158. tapesmith/icons/tabler-index.json +1 -0
  159. tapesmith/imageinput.py +128 -0
  160. tapesmith/install/__init__.py +6 -0
  161. tapesmith/install/installer.py +478 -0
  162. tapesmith/install/junction.py +67 -0
  163. tapesmith/install/layout.py +201 -0
  164. tapesmith/install/legacy.py +231 -0
  165. tapesmith/install/processes.py +221 -0
  166. tapesmith/install/registry.py +112 -0
  167. tapesmith/install/shortcuts.py +109 -0
  168. tapesmith/install/uninstaller.py +213 -0
  169. tapesmith/install/venv.py +241 -0
  170. tapesmith/integration.py +429 -0
  171. tapesmith/integrations/__init__.py +7 -0
  172. tapesmith/integrations/assetnote.py +78 -0
  173. tapesmith/integrations/assets.py +327 -0
  174. tapesmith/integrations/cliprint.py +158 -0
  175. tapesmith/integrations/codescan.py +290 -0
  176. tapesmith/integrations/credentials.py +158 -0
  177. tapesmith/integrations/errors.py +93 -0
  178. tapesmith/integrations/frontmatter.py +189 -0
  179. tapesmith/integrations/homeassistant.py +217 -0
  180. tapesmith/integrations/httpclient.py +91 -0
  181. tapesmith/integrations/kleinanzeigen.py +300 -0
  182. tapesmith/integrations/mcpclient.py +186 -0
  183. tapesmith/integrations/netbox.py +189 -0
  184. tapesmith/integrations/obsidian.py +325 -0
  185. tapesmith/integrations/paperless.py +298 -0
  186. tapesmith/integrations/plausi.py +267 -0
  187. tapesmith/integrations/proxmox.py +438 -0
  188. tapesmith/integrations/scancache.py +99 -0
  189. tapesmith/integrations/settings.py +409 -0
  190. tapesmith/integrations/shortlink.py +169 -0
  191. tapesmith/integrations/snippet.py +143 -0
  192. tapesmith/integrations/tia606.py +218 -0
  193. tapesmith/integrations/winclip.py +76 -0
  194. tapesmith/integrations/zfsreplace.py +336 -0
  195. tapesmith/inventory.py +396 -0
  196. tapesmith/ipc/__init__.py +1 -0
  197. tapesmith/ipc/backend.py +650 -0
  198. tapesmith/ipc/client.py +205 -0
  199. tapesmith/ipc/codec.py +241 -0
  200. tapesmith/ipc/launcher.py +66 -0
  201. tapesmith/ipc/pipe.py +789 -0
  202. tapesmith/ipc/protocol.py +153 -0
  203. tapesmith/jobs.py +80 -0
  204. tapesmith/labelmeta.py +64 -0
  205. tapesmith/launch.py +93 -0
  206. tapesmith/locales/__init__.py +0 -0
  207. tapesmith/locales/de/common.json +18 -0
  208. tapesmith/locales/de/errors.json +222 -0
  209. tapesmith/locales/de/modules.json +60 -0
  210. tapesmith/locales/de/tray.json +114 -0
  211. tapesmith/locales/en/common.json +18 -0
  212. tapesmith/locales/en/errors.json +222 -0
  213. tapesmith/locales/en/modules.json +60 -0
  214. tapesmith/locales/en/tray.json +114 -0
  215. tapesmith/locales/messages/en.json +2592 -0
  216. tapesmith/lock.py +51 -0
  217. tapesmith/mcpserver/__init__.py +5 -0
  218. tapesmith/mcpserver/backends.py +196 -0
  219. tapesmith/mcpserver/server.py +213 -0
  220. tapesmith/mcpserver/tools.py +266 -0
  221. tapesmith/migrate.py +117 -0
  222. tapesmith/modules.py +397 -0
  223. tapesmith/netinfo.py +65 -0
  224. tapesmith/numbering.py +215 -0
  225. tapesmith/pacing.py +8 -0
  226. tapesmith/paths.py +65 -0
  227. tapesmith/pipeline.py +404 -0
  228. tapesmith/printer.py +175 -0
  229. tapesmith/printhooks.py +37 -0
  230. tapesmith/protocol/__init__.py +0 -0
  231. tapesmith/protocol/job.py +77 -0
  232. tapesmith/protocol/raster.py +28 -0
  233. tapesmith/protocol/rawcmd.py +85 -0
  234. tapesmith/protocol/status.py +77 -0
  235. tapesmith/render/__init__.py +0 -0
  236. tapesmith/render/barcode.py +93 -0
  237. tapesmith/render/chain.py +221 -0
  238. tapesmith/render/checks.py +28 -0
  239. tapesmith/render/code128.py +100 -0
  240. tapesmith/render/codes.py +61 -0
  241. tapesmith/render/compose.py +130 -0
  242. tapesmith/render/datamatrix.py +54 -0
  243. tapesmith/render/dither.py +58 -0
  244. tapesmith/render/fixes.py +191 -0
  245. tapesmith/render/fonts.py +46 -0
  246. tapesmith/render/icons.py +309 -0
  247. tapesmith/render/inkcheck.py +65 -0
  248. tapesmith/render/qr.py +78 -0
  249. tapesmith/render/qrcontent.py +175 -0
  250. tapesmith/render/shapes.py +109 -0
  251. tapesmith/render/text.py +120 -0
  252. tapesmith/render/textsize.py +84 -0
  253. tapesmith/render/zxing.py +83 -0
  254. tapesmith/reprint.py +150 -0
  255. tapesmith/secretref.py +149 -0
  256. tapesmith/selftest.py +522 -0
  257. tapesmith/setup.py +141 -0
  258. tapesmith/sourcelimits.py +81 -0
  259. tapesmith/sshscan.py +360 -0
  260. tapesmith/stats.py +96 -0
  261. tapesmith/status.py +122 -0
  262. tapesmith/statusview.py +169 -0
  263. tapesmith/support.py +186 -0
  264. tapesmith/tape/__init__.py +6 -0
  265. tapesmith/tape/preview.py +50 -0
  266. tapesmith/tape/profiles.py +179 -0
  267. tapesmith/tape/rolls.py +187 -0
  268. tapesmith/tape/suitability.py +69 -0
  269. tapesmith/tape/tapes.json +22 -0
  270. tapesmith/templates/__init__.py +0 -0
  271. tapesmith/templates/builtin/__init__.py +0 -0
  272. tapesmith/templates/builtin/absender.tapesmith.json +29 -0
  273. tapesmith/templates/builtin/asn.tapesmith.json +30 -0
  274. tapesmith/templates/builtin/asset-kurz.tapesmith.json +34 -0
  275. tapesmith/templates/builtin/asset-tag.tapesmith.json +32 -0
  276. tapesmith/templates/builtin/aufbewahrungsbox.tapesmith.json +34 -0
  277. tapesmith/templates/builtin/batterie.tapesmith.json +32 -0
  278. tapesmith/templates/builtin/datentraeger-qr.tapesmith.json +27 -0
  279. tapesmith/templates/builtin/datentraeger.tapesmith.json +27 -0
  280. tapesmith/templates/builtin/eigentum.tapesmith.json +32 -0
  281. tapesmith/templates/builtin/garantie-qr.tapesmith.json +37 -0
  282. tapesmith/templates/builtin/garantie.tapesmith.json +31 -0
  283. tapesmith/templates/builtin/gefriergut.tapesmith.json +43 -0
  284. tapesmith/templates/builtin/geoeffnet-am.tapesmith.json +28 -0
  285. tapesmith/templates/builtin/host-ip.tapesmith.json +35 -0
  286. tapesmith/templates/builtin/ip-label.tapesmith.json +25 -0
  287. tapesmith/templates/builtin/ka-artikel.tapesmith.json +36 -0
  288. tapesmith/templates/builtin/kabelfahne.tapesmith.json +37 -0
  289. tapesmith/templates/builtin/kabelwickel.tapesmith.json +29 -0
  290. tapesmith/templates/builtin/netzteil-warnung.tapesmith.json +36 -0
  291. tapesmith/templates/builtin/netzteil-zuordnung.tapesmith.json +31 -0
  292. tapesmith/templates/builtin/netzteil.tapesmith.json +31 -0
  293. tapesmith/templates/builtin/ordnerruecken-breit.tapesmith.json +27 -0
  294. tapesmith/templates/builtin/ordnerruecken-schmal.tapesmith.json +27 -0
  295. tapesmith/templates/builtin/passthrough.tapesmith.json +31 -0
  296. tapesmith/templates/builtin/platte-defekt.tapesmith.json +32 -0
  297. tapesmith/templates/builtin/preis.tapesmith.json +26 -0
  298. tapesmith/templates/builtin/psu-paar.tapesmith.json +32 -0
  299. tapesmith/templates/builtin/raster-patchpanel.tapesmith.json +26 -0
  300. tapesmith/templates/builtin/raster-sicherungskasten.tapesmith.json +25 -0
  301. tapesmith/templates/builtin/raster-sortiment.tapesmith.json +25 -0
  302. tapesmith/templates/builtin/raster-switch.tapesmith.json +26 -0
  303. tapesmith/templates/builtin/reserviert.tapesmith.json +27 -0
  304. tapesmith/templates/builtin/schule.tapesmith.json +38 -0
  305. tapesmith/templates/builtin/vm-lxc-qr.tapesmith.json +37 -0
  306. tapesmith/templates/builtin/vm-lxc.tapesmith.json +29 -0
  307. tapesmith/templates/builtin/vorratsdose.tapesmith.json +37 -0
  308. tapesmith/templates/builtin/wartung.tapesmith.json +35 -0
  309. tapesmith/templates/fill.py +183 -0
  310. tapesmith/templates/gallery.py +239 -0
  311. tapesmith/templates/lint.py +191 -0
  312. tapesmith/templates/model.py +849 -0
  313. tapesmith/templates/render.py +245 -0
  314. tapesmith/templates/serial.py +54 -0
  315. tapesmith/templates/series.py +130 -0
  316. tapesmith/templates/store.py +110 -0
  317. tapesmith/transport/__init__.py +0 -0
  318. tapesmith/transport/base.py +121 -0
  319. tapesmith/transport/ble.py +239 -0
  320. tapesmith/transport/btports.py +70 -0
  321. tapesmith/transport/resolve.py +50 -0
  322. tapesmith/transport/serial_port.py +72 -0
  323. tapesmith/transport/usb.py +289 -0
  324. tapesmith/update/__init__.py +10 -0
  325. tapesmith/update/apply.py +310 -0
  326. tapesmith/update/errors.py +23 -0
  327. tapesmith/update/manifest.py +278 -0
  328. tapesmith/update/service.py +394 -0
  329. tapesmith/update/signing.py +93 -0
  330. tapesmith/update/sources.py +308 -0
  331. tapesmith/update/state.py +67 -0
  332. tapesmith/update/trusted_keys.json +8 -0
  333. tapesmith/verify.py +136 -0
  334. tapesmith/webapi/__init__.py +5 -0
  335. tapesmith/webapi/accent.py +26 -0
  336. tapesmith/webapi/access.py +144 -0
  337. tapesmith/webapi/actions.py +115 -0
  338. tapesmith/webapi/app.py +124 -0
  339. tapesmith/webapi/batchapi.py +150 -0
  340. tapesmith/webapi/context.py +125 -0
  341. tapesmith/webapi/convert.py +51 -0
  342. tapesmith/webapi/datajson.py +95 -0
  343. tapesmith/webapi/documents.py +93 -0
  344. tapesmith/webapi/drafts.py +226 -0
  345. tapesmith/webapi/errors.py +197 -0
  346. tapesmith/webapi/events.py +105 -0
  347. tapesmith/webapi/homelab_common.py +61 -0
  348. tapesmith/webapi/homelab_routers.py +30 -0
  349. tapesmith/webapi/labels.py +575 -0
  350. tapesmith/webapi/language.py +52 -0
  351. tapesmith/webapi/mcp_http.py +65 -0
  352. tapesmith/webapi/previews.py +70 -0
  353. tapesmith/webapi/printing.py +110 -0
  354. tapesmith/webapi/ratelimit.py +93 -0
  355. tapesmith/webapi/routes_access.py +210 -0
  356. tapesmith/webapi/routes_assets.py +213 -0
  357. tapesmith/webapi/routes_codescan.py +58 -0
  358. tapesmith/webapi/routes_core.py +148 -0
  359. tapesmith/webapi/routes_data.py +516 -0
  360. tapesmith/webapi/routes_drafts.py +74 -0
  361. tapesmith/webapi/routes_editor.py +396 -0
  362. tapesmith/webapi/routes_family.py +270 -0
  363. tapesmith/webapi/routes_ha.py +106 -0
  364. tapesmith/webapi/routes_homelab.py +69 -0
  365. tapesmith/webapi/routes_kabel.py +218 -0
  366. tapesmith/webapi/routes_kleinanzeigen.py +145 -0
  367. tapesmith/webapi/routes_labels.py +74 -0
  368. tapesmith/webapi/routes_modules.py +78 -0
  369. tapesmith/webapi/routes_paperless.py +116 -0
  370. tapesmith/webapi/routes_plausi.py +133 -0
  371. tapesmith/webapi/routes_proxmox.py +93 -0
  372. tapesmith/webapi/routes_settings.py +327 -0
  373. tapesmith/webapi/routes_short.py +203 -0
  374. tapesmith/webapi/routes_support.py +46 -0
  375. tapesmith/webapi/routes_system.py +179 -0
  376. tapesmith/webapi/routes_templates.py +474 -0
  377. tapesmith/webapi/routes_update.py +154 -0
  378. tapesmith/webapi/routes_vault.py +202 -0
  379. tapesmith/webapi/routes_zfs.py +103 -0
  380. tapesmith/webapi/security.py +254 -0
  381. tapesmith/webapi/server.py +229 -0
  382. tapesmith/webapi/session.py +70 -0
  383. tapesmith/webapi/settings_schema.py +462 -0
  384. tapesmith/webui/__init__.py +8 -0
  385. tapesmith/webui/browser.py +261 -0
  386. tapesmith/webui/static/assets/DataList-BNdLqY0h.js +1 -0
  387. tapesmith/webui/static/assets/FamilyApp-ArkuANKE.js +1 -0
  388. tapesmith/webui/static/assets/ListToolbar-C4dAZi00.js +1 -0
  389. tapesmith/webui/static/assets/ModuleSettingsButton-ZUbj_1ik.js +1 -0
  390. tapesmith/webui/static/assets/NavList-Dwey-1PS.js +1 -0
  391. tapesmith/webui/static/assets/PrintOptionsBar-gdugjiWW.js +1 -0
  392. tapesmith/webui/static/assets/RowActions-CG90mya5.js +1 -0
  393. tapesmith/webui/static/assets/Section-B0h_MINH.js +1 -0
  394. tapesmith/webui/static/assets/TableScroll-BWJfFpya.js +1 -0
  395. tapesmith/webui/static/assets/TapePreview-DKSbEPDH.js +1 -0
  396. tapesmith/webui/static/assets/TextSizeSelect-NmOXQvqm.js +1 -0
  397. tapesmith/webui/static/assets/WarningList-TFhCJHVc.js +1 -0
  398. tapesmith/webui/static/assets/api-DAWnziWm.js +1 -0
  399. tapesmith/webui/static/assets/fluent-DAQMrKDT.js +194 -0
  400. tapesmith/webui/static/assets/gateMessages-BjG-W2n9.js +8 -0
  401. tapesmith/webui/static/assets/index-B0ibC1fA.js +5 -0
  402. tapesmith/webui/static/assets/index-B3ieSe8A.js +1 -0
  403. tapesmith/webui/static/assets/index-B6LmWkwg.js +1 -0
  404. tapesmith/webui/static/assets/index-BBGAZEVC.js +1 -0
  405. tapesmith/webui/static/assets/index-BFOUDlKz.js +1 -0
  406. tapesmith/webui/static/assets/index-BG0glZqe.js +2 -0
  407. tapesmith/webui/static/assets/index-Bk_o53YF.js +2 -0
  408. tapesmith/webui/static/assets/index-Bm_YhjSS.js +39 -0
  409. tapesmith/webui/static/assets/index-C1culFZj.js +2 -0
  410. tapesmith/webui/static/assets/index-C7z4MrlV.js +1 -0
  411. tapesmith/webui/static/assets/index-C8Wfr4XK.js +1 -0
  412. tapesmith/webui/static/assets/index-CSBHxRZo.js +4 -0
  413. tapesmith/webui/static/assets/index-CUM2lQP7.js +7 -0
  414. tapesmith/webui/static/assets/index-CUc32c9J.js +1 -0
  415. tapesmith/webui/static/assets/index-CYOf3IW7.js +1 -0
  416. tapesmith/webui/static/assets/index-CqdKpquX.js +1 -0
  417. tapesmith/webui/static/assets/index-DRDqtjoV.js +2 -0
  418. tapesmith/webui/static/assets/index-DXW3DZbF.js +1 -0
  419. tapesmith/webui/static/assets/index-NbuszyXs.js +3 -0
  420. tapesmith/webui/static/assets/index-QLKmO2qo.js +1 -0
  421. tapesmith/webui/static/assets/index-TA-i_7q_.js +6 -0
  422. tapesmith/webui/static/assets/index-qLvSlZoG.js +1 -0
  423. tapesmith/webui/static/assets/index-sdE0n6Yf.js +1 -0
  424. tapesmith/webui/static/assets/index-vkhP0yaY.js +2 -0
  425. tapesmith/webui/static/assets/konva-CZCmb_o2.js +51 -0
  426. tapesmith/webui/static/assets/numberText-CaqySO_R.js +1 -0
  427. tapesmith/webui/static/assets/systemTitle-qe0pdYpR.js +1 -0
  428. tapesmith/webui/static/assets/time-Ctx9DKCc.js +1 -0
  429. tapesmith/webui/static/assets/unsaved-Dbna7DRF.js +1 -0
  430. tapesmith/webui/static/index.html +19 -0
  431. tapesmith-0.4.0.dist-info/METADATA +246 -0
  432. tapesmith-0.4.0.dist-info/RECORD +436 -0
  433. tapesmith-0.4.0.dist-info/WHEEL +4 -0
  434. tapesmith-0.4.0.dist-info/entry_points.txt +6 -0
  435. tapesmith-0.4.0.dist-info/licenses/LICENSE +21 -0
  436. 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)