zebra-day 1.0.2__py3-none-any.whl → 2.1.4__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 (174) hide show
  1. zebra_day/__init__.py +7 -2
  2. zebra_day/_version.py +1 -0
  3. zebra_day/cli/__init__.py +80 -30
  4. zebra_day/cli/cognito.py +15 -9
  5. zebra_day/cli/gui.py +101 -13
  6. zebra_day/cli/printer.py +34 -27
  7. zebra_day/cli/template.py +19 -15
  8. zebra_day/cmd_mgr.py +3 -6
  9. zebra_day/docs/gx420d-gx430d-ug-en.pdf +0 -0
  10. zebra_day/docs/hardware_config_guide.md +149 -0
  11. zebra_day/docs/programatic_guide.md +181 -0
  12. zebra_day/docs/qln420_zebra_manual.pdf +0 -0
  13. zebra_day/docs/uid_screed_light.md +38 -0
  14. zebra_day/docs/zd620-zd420-ug-en.pdf +0 -0
  15. zebra_day/docs/zebra_day_ui_guide.md +194 -0
  16. zebra_day/etc/printer_config.json +5 -11
  17. zebra_day/etc/printer_config.template.json +5 -11
  18. zebra_day/etc/tmp_printers120.json +10 -0
  19. zebra_day/etc/tmp_printers139.json +10 -0
  20. zebra_day/etc/tmp_printers145.json +10 -0
  21. zebra_day/etc/tmp_printers147.json +10 -0
  22. zebra_day/etc/tmp_printers207.json +10 -0
  23. zebra_day/etc/tmp_printers34.json +10 -0
  24. zebra_day/etc/tmp_printers389.json +10 -0
  25. zebra_day/etc/tmp_printers398.json +10 -0
  26. zebra_day/etc/tmp_printers437.json +10 -0
  27. zebra_day/etc/tmp_printers439.json +10 -0
  28. zebra_day/etc/tmp_printers440.json +10 -0
  29. zebra_day/etc/tmp_printers469.json +10 -0
  30. zebra_day/etc/tmp_printers485.json +10 -0
  31. zebra_day/etc/tmp_printers508.json +10 -0
  32. zebra_day/etc/tmp_printers531.json +10 -0
  33. zebra_day/etc/tmp_printers540.json +10 -0
  34. zebra_day/etc/tmp_printers542.json +10 -0
  35. zebra_day/etc/tmp_printers543.json +10 -0
  36. zebra_day/etc/tmp_printers552.json +10 -0
  37. zebra_day/etc/tmp_printers715.json +10 -0
  38. zebra_day/etc/tmp_printers835.json +10 -0
  39. zebra_day/etc/tmp_printers842.json +10 -0
  40. zebra_day/etc/tmp_printers931.json +10 -0
  41. zebra_day/etc/tmp_printers969.json +10 -0
  42. zebra_day/etc/tmp_printers972.json +10 -0
  43. zebra_day/exceptions.py +1 -1
  44. zebra_day/files/blank_preview.png +0 -0
  45. zebra_day/files/corners_20cmX30cm_preview.png +0 -0
  46. zebra_day/files/corners_smallTube_preview.png +0 -0
  47. zebra_day/files/generic_2inX1in_preview.png +0 -0
  48. zebra_day/files/test_png_12020.png +0 -0
  49. zebra_day/files/test_png_12352.png +0 -0
  50. zebra_day/files/test_png_15472.png +0 -0
  51. zebra_day/files/test_png_24493.png +0 -0
  52. zebra_day/files/test_png_2897.png +0 -0
  53. zebra_day/files/test_png_30069.png +0 -0
  54. zebra_day/files/test_png_31690.png +0 -0
  55. zebra_day/files/test_png_33804.png +0 -0
  56. zebra_day/files/test_png_34737.png +0 -0
  57. zebra_day/files/test_png_4161.png +0 -0
  58. zebra_day/files/test_png_44748.png +0 -0
  59. zebra_day/files/test_png_4635.png +0 -0
  60. zebra_day/files/test_png_47791.png +0 -0
  61. zebra_day/files/test_png_47799.png +0 -0
  62. zebra_day/files/test_png_55588.png +0 -0
  63. zebra_day/files/test_png_56349.png +0 -0
  64. zebra_day/files/test_png_58809.png +0 -0
  65. zebra_day/files/test_png_5936.png +0 -0
  66. zebra_day/files/test_png_64110.png +0 -0
  67. zebra_day/files/test_png_64891.png +0 -0
  68. zebra_day/files/test_png_67242.png +0 -0
  69. zebra_day/files/test_png_69002.png +0 -0
  70. zebra_day/files/test_png_70065.png +0 -0
  71. zebra_day/files/test_png_72366.png +0 -0
  72. zebra_day/files/test_png_77793.png +0 -0
  73. zebra_day/files/test_png_89893.png +0 -0
  74. zebra_day/files/test_png_9572.png +0 -0
  75. zebra_day/files/tube_20mmX30mmA_preview.png +0 -0
  76. zebra_day/imgs/.hold +0 -0
  77. zebra_day/imgs/bar_ltpurp.png +0 -0
  78. zebra_day/imgs/bar_purp.png +0 -0
  79. zebra_day/imgs/bar_purp3.png +0 -0
  80. zebra_day/imgs/bar_red.png +0 -0
  81. zebra_day/imgs/legacy/UBC_gantt_chart.png +0 -0
  82. zebra_day/imgs/legacy/gx420d_network_config.png +0 -0
  83. zebra_day/imgs/legacy/gx420d_printer_config.png +0 -0
  84. zebra_day/imgs/legacy/ngrok.png +0 -0
  85. zebra_day/imgs/legacy/printer_details.png +0 -0
  86. zebra_day/imgs/legacy/quick_start_test_label.png +0 -0
  87. zebra_day/imgs/legacy/quick_start_test_label2.png +0 -0
  88. zebra_day/imgs/legacy/zd620_network_config.png +0 -0
  89. zebra_day/imgs/legacy/zd620_printer_config.png +0 -0
  90. zebra_day/imgs/legacy/zday_quick_gui.png +0 -0
  91. zebra_day/imgs/legacy/zebra_day_alt_css_dog.png +0 -0
  92. zebra_day/imgs/legacy/zebra_day_alt_css_flower.png +0 -0
  93. zebra_day/imgs/legacy/zebra_day_alt_css_main.png +0 -0
  94. zebra_day/imgs/legacy/zebra_day_available_zpl_templates.png +0 -0
  95. zebra_day/imgs/legacy/zebra_day_bkup_pconfig.png +0 -0
  96. zebra_day/imgs/legacy/zebra_day_home.png +0 -0
  97. zebra_day/imgs/legacy/zebra_day_manual_print.png +0 -0
  98. zebra_day/imgs/legacy/zebra_day_printer_fleet_json.png +0 -0
  99. zebra_day/imgs/legacy/zebra_day_quick_ex.png +0 -0
  100. zebra_day/imgs/legacy/zebra_day_zpl_template_IRLa.png +0 -0
  101. zebra_day/imgs/legacy/zebra_day_zpl_template_IRLb.png +0 -0
  102. zebra_day/imgs/ui_api_docs.png +0 -0
  103. zebra_day/imgs/ui_config.png +0 -0
  104. zebra_day/imgs/ui_dashboard.png +0 -0
  105. zebra_day/imgs/ui_print_request.png +0 -0
  106. zebra_day/imgs/ui_printers.png +0 -0
  107. zebra_day/imgs/ui_templates.png +0 -0
  108. zebra_day/logging_config.py +4 -9
  109. zebra_day/mkcert.py +157 -0
  110. zebra_day/paths.py +1 -2
  111. zebra_day/print_mgr.py +261 -185
  112. zebra_day/templates/modern/config.html +7 -0
  113. zebra_day/templates/modern/config_backups.html +59 -0
  114. zebra_day/templates/modern/config_editor.html +95 -0
  115. zebra_day/templates/modern/config_new.html +93 -0
  116. zebra_day/templates/modern/print_request.html +70 -8
  117. zebra_day/templates/modern/printer_detail.html +161 -34
  118. zebra_day/templates/modern/printers.html +17 -6
  119. zebra_day/templates/modern/template_editor.html +7 -4
  120. zebra_day/web/__init__.py +1 -1
  121. zebra_day/web/app.py +99 -17
  122. zebra_day/web/auth.py +17 -15
  123. zebra_day/web/middleware.py +8 -5
  124. zebra_day/web/routers/__init__.py +0 -1
  125. zebra_day/web/routers/api.py +330 -31
  126. zebra_day/web/routers/ui.py +174 -591
  127. zebra_day/zpl_renderer.py +45 -34
  128. {zebra_day-1.0.2.dist-info → zebra_day-2.1.4.dist-info}/METADATA +144 -74
  129. zebra_day-2.1.4.dist-info/RECORD +240 -0
  130. zebra_day/bin/fetch_zebra_config.py +0 -15
  131. zebra_day/bin/generate_coord_grid_zpl.py +0 -50
  132. zebra_day/bin/print_zpl_from_file.py +0 -21
  133. zebra_day/bin/probe_new_label_dimensions.py +0 -75
  134. zebra_day/bin/scan_for_networed_zebra_printers.py +0 -23
  135. zebra_day/bin/scan_for_networed_zebra_printers_arp_scan.sh +0 -1
  136. zebra_day/bin/scan_for_networed_zebra_printers_curl.sh +0 -30
  137. zebra_day/bin/zserve.py +0 -1062
  138. zebra_day/templates/base.html +0 -36
  139. zebra_day/templates/bpr.html +0 -72
  140. zebra_day/templates/build_new_config.html +0 -36
  141. zebra_day/templates/build_print_request.html +0 -32
  142. zebra_day/templates/chg_ui_style.html +0 -19
  143. zebra_day/templates/edit_template.html +0 -128
  144. zebra_day/templates/edit_zpl.html +0 -37
  145. zebra_day/templates/index.html +0 -82
  146. zebra_day/templates/legacy/base.html +0 -37
  147. zebra_day/templates/legacy/bpr.html +0 -72
  148. zebra_day/templates/legacy/build_new_config.html +0 -36
  149. zebra_day/templates/legacy/build_print_request.html +0 -32
  150. zebra_day/templates/legacy/chg_ui_style.html +0 -19
  151. zebra_day/templates/legacy/edit_template.html +0 -128
  152. zebra_day/templates/legacy/edit_zpl.html +0 -37
  153. zebra_day/templates/legacy/index.html +0 -82
  154. zebra_day/templates/legacy/list_prior_configs.html +0 -24
  155. zebra_day/templates/legacy/print_result.html +0 -30
  156. zebra_day/templates/legacy/printer_details.html +0 -25
  157. zebra_day/templates/legacy/printer_status.html +0 -70
  158. zebra_day/templates/legacy/save_result.html +0 -17
  159. zebra_day/templates/legacy/send_print_request.html +0 -34
  160. zebra_day/templates/legacy/simple_print.html +0 -94
  161. zebra_day/templates/legacy/view_pstation_json.html +0 -29
  162. zebra_day/templates/list_prior_configs.html +0 -24
  163. zebra_day/templates/print_result.html +0 -30
  164. zebra_day/templates/printer_details.html +0 -25
  165. zebra_day/templates/printer_status.html +0 -70
  166. zebra_day/templates/save_result.html +0 -17
  167. zebra_day/templates/send_print_request.html +0 -34
  168. zebra_day/templates/simple_print.html +0 -94
  169. zebra_day/templates/view_pstation_json.html +0 -29
  170. zebra_day-1.0.2.dist-info/RECORD +0 -179
  171. {zebra_day-1.0.2.dist-info → zebra_day-2.1.4.dist-info}/WHEEL +0 -0
  172. {zebra_day-1.0.2.dist-info → zebra_day-2.1.4.dist-info}/entry_points.txt +0 -0
  173. {zebra_day-1.0.2.dist-info → zebra_day-2.1.4.dist-info}/licenses/LICENSE +0 -0
  174. {zebra_day-1.0.2.dist-info → zebra_day-2.1.4.dist-info}/top_level.txt +0 -0
@@ -4,23 +4,30 @@ Versioned JSON API router for zebra_day.
4
4
  Provides programmatic access to printer management and label printing.
5
5
  All endpoints return JSON and are prefixed with /api/v1/.
6
6
  """
7
+
7
8
  from __future__ import annotations
8
9
 
9
- from typing import Optional, List, Dict, Any
10
+ from datetime import datetime
11
+ from typing import Any
10
12
 
11
13
  from fastapi import APIRouter, HTTPException, Request
14
+ from fastapi.responses import FileResponse
12
15
  from pydantic import BaseModel, Field
13
16
 
17
+ from zebra_day import paths as xdg
18
+
14
19
  router = APIRouter()
15
20
 
16
21
 
17
22
  # ----- Request/Response Models -----
18
23
 
24
+
19
25
  class PrintRequest(BaseModel):
20
26
  """Request model for printing a label."""
27
+
21
28
  lab: str = Field(..., description="Lab identifier")
22
29
  printer: str = Field(..., description="Printer name")
23
- label_zpl_style: Optional[str] = Field(None, description="ZPL template name")
30
+ label_zpl_style: str | None = Field(None, description="ZPL template name")
24
31
  uid_barcode: str = Field("", description="UID for barcode")
25
32
  alt_a: str = Field("", description="Alternative field A")
26
33
  alt_b: str = Field("", description="Alternative field B")
@@ -33,38 +40,122 @@ class PrintRequest(BaseModel):
33
40
 
34
41
  class PrintResponse(BaseModel):
35
42
  """Response model for print request."""
43
+
36
44
  success: bool
37
45
  message: str
38
- png_url: Optional[str] = None
46
+ png_url: str | None = None
39
47
 
40
48
 
41
49
  class PrinterInfo(BaseModel):
42
- """Printer information model."""
43
- name: str
50
+ """Printer information model (v2.0.0 schema)."""
51
+
52
+ id: str = Field(..., description="Printer identifier/key in JSON")
44
53
  ip_address: str
54
+ printer_name: str | None = Field(None, description="User-friendly display name")
55
+ lab_location: str | None = Field(None, description="Location within the lab")
56
+ manufacturer: str = Field("zebra", description="Printer manufacturer")
45
57
  model: str
46
58
  serial: str
47
- label_zpl_styles: List[str]
59
+ label_zpl_styles: list[str]
60
+ default_label_style: str | None = Field(
61
+ None, description="Default label style to use when none specified"
62
+ )
48
63
  print_method: str
64
+ notes: str | None = Field("", description="Optional notes")
65
+
66
+
67
+ class LabInfo(BaseModel):
68
+ """Lab information model (v2.0.0 schema)."""
69
+
70
+ id: str = Field(..., description="Lab identifier/key in JSON")
71
+ lab_name: str = Field(..., description="Human-readable lab name")
72
+ available_locations: list[str] = Field(
73
+ default_factory=list, description="Valid location options for printers"
74
+ )
75
+ printers: list[PrinterInfo]
49
76
 
50
77
 
51
78
  class LabPrinters(BaseModel):
52
- """Lab and its printers."""
79
+ """Lab and its printers (deprecated, use LabInfo)."""
80
+
53
81
  lab: str
54
- printers: List[PrinterInfo]
82
+ printers: list[PrinterInfo]
83
+
84
+
85
+ class RenderRequest(BaseModel):
86
+ """Request model for rendering ZPL to PNG."""
87
+
88
+ template: str | None = Field(None, description="ZPL template name (e.g., 'tube_2inX1in')")
89
+ zpl_content: str | None = Field(
90
+ None, description="Raw ZPL content (takes precedence over template)"
91
+ )
92
+ uid_barcode: str = Field("", description="UID for barcode")
93
+ alt_a: str = Field("", description="Alternative field A")
94
+ alt_b: str = Field("", description="Alternative field B")
95
+ alt_c: str = Field("", description="Alternative field C")
96
+ alt_d: str = Field("", description="Alternative field D")
97
+ alt_e: str = Field("", description="Alternative field E")
98
+ alt_f: str = Field("", description="Alternative field F")
99
+
100
+
101
+ class RenderResponse(BaseModel):
102
+ """Response model for render request (when not returning PNG directly)."""
103
+
104
+ success: bool
105
+ message: str
106
+ png_url: str = Field(..., description="URL to download the generated PNG")
55
107
 
56
108
 
57
109
  # ----- Endpoints -----
58
110
 
59
- @router.get("/labs", response_model=List[str])
60
- async def list_labs(request: Request) -> List[str]:
111
+
112
+ @router.get("/labs", response_model=list[str])
113
+ async def list_labs(request: Request) -> list[str]:
61
114
  """List all available labs."""
62
115
  zp = request.app.state.zp
63
116
  return list(zp.printers.get("labs", {}).keys())
64
117
 
65
118
 
66
- @router.get("/labs/{lab}/printers", response_model=List[PrinterInfo])
67
- async def list_printers(request: Request, lab: str) -> List[PrinterInfo]:
119
+ @router.get("/labs/{lab}", response_model=LabInfo)
120
+ async def get_lab(request: Request, lab: str) -> LabInfo:
121
+ """Get lab details including available locations and printers."""
122
+ zp = request.app.state.zp
123
+ labs = zp.printers.get("labs", {})
124
+
125
+ if lab not in labs:
126
+ raise HTTPException(status_code=404, detail=f"Lab '{lab}' not found")
127
+
128
+ lab_data = labs[lab]
129
+ lab_printers = lab_data.get("printers", {})
130
+
131
+ printers = []
132
+ for printer_id, info in lab_printers.items():
133
+ printers.append(
134
+ PrinterInfo(
135
+ id=printer_id,
136
+ ip_address=info.get("ip_address", ""),
137
+ printer_name=info.get("printer_name"),
138
+ lab_location=info.get("lab_location"),
139
+ manufacturer=info.get("manufacturer", "zebra"),
140
+ model=info.get("model", ""),
141
+ serial=info.get("serial", ""),
142
+ label_zpl_styles=info.get("label_zpl_styles", []),
143
+ default_label_style=info.get("default_label_style"),
144
+ print_method=info.get("print_method", ""),
145
+ notes=info.get("notes", ""),
146
+ )
147
+ )
148
+
149
+ return LabInfo(
150
+ id=lab,
151
+ lab_name=lab_data.get("lab_name", lab),
152
+ available_locations=lab_data.get("available_locations", []),
153
+ printers=printers,
154
+ )
155
+
156
+
157
+ @router.get("/labs/{lab}/printers", response_model=list[PrinterInfo])
158
+ async def list_printers(request: Request, lab: str) -> list[PrinterInfo]:
68
159
  """List all printers in a lab."""
69
160
  zp = request.app.state.zp
70
161
  labs = zp.printers.get("labs", {})
@@ -72,25 +163,32 @@ async def list_printers(request: Request, lab: str) -> List[PrinterInfo]:
72
163
  if lab not in labs:
73
164
  raise HTTPException(status_code=404, detail=f"Lab '{lab}' not found")
74
165
 
166
+ # Access printers via nested 'printers' key (v2 schema)
167
+ lab_printers = labs[lab].get("printers", {})
168
+
75
169
  printers = []
76
- for name, info in labs[lab].items():
170
+ for printer_id, info in lab_printers.items():
77
171
  printers.append(
78
172
  PrinterInfo(
79
- name=name,
173
+ id=printer_id,
80
174
  ip_address=info.get("ip_address", ""),
175
+ printer_name=info.get("printer_name"),
176
+ lab_location=info.get("lab_location"),
177
+ manufacturer=info.get("manufacturer", "zebra"),
81
178
  model=info.get("model", ""),
82
179
  serial=info.get("serial", ""),
83
180
  label_zpl_styles=info.get("label_zpl_styles", []),
181
+ default_label_style=info.get("default_label_style"),
84
182
  print_method=info.get("print_method", ""),
183
+ notes=info.get("notes", ""),
85
184
  )
86
185
  )
87
186
  return printers
88
187
 
89
188
 
90
- @router.get("/templates", response_model=List[str])
91
- async def list_templates(request: Request) -> List[str]:
189
+ @router.get("/templates", response_model=list[str])
190
+ async def list_templates(request: Request) -> list[str]:
92
191
  """List all available ZPL templates."""
93
- from pathlib import Path
94
192
 
95
193
  pkg_path = request.app.state.pkg_path
96
194
  styles_dir = pkg_path / "etc" / "label_styles"
@@ -124,7 +222,7 @@ async def print_label(request: Request, print_req: PrintRequest) -> PrintRespons
124
222
  raise HTTPException(status_code=429, detail=reason)
125
223
 
126
224
  try:
127
- result = zp.print_zpl(
225
+ zp.print_zpl(
128
226
  lab=print_req.lab,
129
227
  printer_name=print_req.printer,
130
228
  label_zpl_style=print_req.label_zpl_style,
@@ -138,26 +236,227 @@ async def print_label(request: Request, print_req: PrintRequest) -> PrintRespons
138
236
  print_n=print_req.copies,
139
237
  client_ip=client_ip,
140
238
  )
141
-
142
- # Check if result is a PNG file path
143
- if result and ".png" in str(result):
144
- png_name = str(result).split("/")[-1]
145
- return PrintResponse(
146
- success=True,
147
- message="PNG generated successfully",
148
- png_url=f"/files/{png_name}",
149
- )
150
-
151
239
  return PrintResponse(success=True, message="Print request sent successfully")
152
240
  except Exception as e:
153
- raise HTTPException(status_code=500, detail=str(e))
241
+ raise HTTPException(status_code=500, detail=str(e)) from None
154
242
  finally:
155
243
  rate_limiter.release()
156
244
 
157
245
 
246
+ @router.post("/render", response_model=RenderResponse)
247
+ async def render_label(request: Request, render_req: RenderRequest) -> RenderResponse:
248
+ """
249
+ Render ZPL to PNG image.
250
+
251
+ This endpoint generates a PNG image from ZPL content without sending to a printer.
252
+ You can provide either:
253
+ - A template name (e.g., 'tube_2inX1in') with field values
254
+ - Raw ZPL content directly
255
+
256
+ Returns a URL to download the generated PNG.
257
+ """
258
+ zp = request.app.state.zp
259
+
260
+ # Validate that we have either template or zpl_content
261
+ if not render_req.template and not render_req.zpl_content:
262
+ raise HTTPException(
263
+ status_code=400, detail="Either 'template' or 'zpl_content' must be provided"
264
+ )
265
+
266
+ try:
267
+ # Generate ZPL string from template if not provided directly
268
+ if render_req.zpl_content:
269
+ zpl_string = render_req.zpl_content
270
+ else:
271
+ zpl_string = zp.formulate_zpl(
272
+ uid_barcode=render_req.uid_barcode,
273
+ alt_a=render_req.alt_a,
274
+ alt_b=render_req.alt_b,
275
+ alt_c=render_req.alt_c,
276
+ alt_d=render_req.alt_d,
277
+ alt_e=render_req.alt_e,
278
+ alt_f=render_req.alt_f,
279
+ label_zpl_style=render_req.template,
280
+ )
281
+
282
+ # Generate unique filename
283
+ timestamp = datetime.now().strftime("%Y-%m-%d_%H:%M:%S.%f")
284
+ template_name = render_req.template or "custom"
285
+ png_filename = f"zpl_render_{template_name}_{timestamp}.png"
286
+ png_path = xdg.get_generated_files_dir() / png_filename
287
+
288
+ # Render to PNG
289
+ zp.generate_label_png(zpl_string, str(png_path), relative=False)
290
+
291
+ return RenderResponse(
292
+ success=True,
293
+ message="PNG rendered successfully",
294
+ png_url=f"/generated/{png_filename}",
295
+ )
296
+ except FileNotFoundError as e:
297
+ raise HTTPException(status_code=404, detail=f"Template not found: {e}") from None
298
+ except Exception as e:
299
+ raise HTTPException(status_code=500, detail=f"Render failed: {e}") from None
300
+
301
+
302
+ @router.post("/render/png")
303
+ async def render_label_png(request: Request, render_req: RenderRequest):
304
+ """
305
+ Render ZPL to PNG and return the image directly.
306
+
307
+ Same as /render but returns the PNG file directly instead of a URL.
308
+ Useful for programmatic access where you want the image bytes.
309
+ """
310
+ zp = request.app.state.zp
311
+
312
+ # Validate that we have either template or zpl_content
313
+ if not render_req.template and not render_req.zpl_content:
314
+ raise HTTPException(
315
+ status_code=400, detail="Either 'template' or 'zpl_content' must be provided"
316
+ )
317
+
318
+ try:
319
+ # Generate ZPL string from template if not provided directly
320
+ if render_req.zpl_content:
321
+ zpl_string = render_req.zpl_content
322
+ else:
323
+ zpl_string = zp.formulate_zpl(
324
+ uid_barcode=render_req.uid_barcode,
325
+ alt_a=render_req.alt_a,
326
+ alt_b=render_req.alt_b,
327
+ alt_c=render_req.alt_c,
328
+ alt_d=render_req.alt_d,
329
+ alt_e=render_req.alt_e,
330
+ alt_f=render_req.alt_f,
331
+ label_zpl_style=render_req.template,
332
+ )
333
+
334
+ # Generate unique filename
335
+ timestamp = datetime.now().strftime("%Y-%m-%d_%H:%M:%S.%f")
336
+ template_name = render_req.template or "custom"
337
+ png_filename = f"zpl_render_{template_name}_{timestamp}.png"
338
+ png_path = xdg.get_generated_files_dir() / png_filename
339
+
340
+ # Render to PNG
341
+ zp.generate_label_png(zpl_string, str(png_path), relative=False)
342
+
343
+ # Return the file directly
344
+ return FileResponse(
345
+ path=str(png_path),
346
+ media_type="image/png",
347
+ filename=png_filename,
348
+ )
349
+ except FileNotFoundError as e:
350
+ raise HTTPException(status_code=404, detail=f"Template not found: {e}") from None
351
+ except Exception as e:
352
+ raise HTTPException(status_code=500, detail=f"Render failed: {e}") from None
353
+
354
+
158
355
  @router.get("/config")
159
- async def get_config(request: Request) -> Dict[str, Any]:
356
+ async def get_config(request: Request) -> dict[str, Any]:
160
357
  """Get the current printer configuration."""
161
358
  zp = request.app.state.zp
162
- return zp.printers
359
+ return dict(zp.printers)
360
+
361
+
362
+ # ----- Lab Settings Endpoints -----
363
+
364
+
365
+ class LabUpdateRequest(BaseModel):
366
+ """Request model for updating lab settings."""
367
+
368
+ lab_name: str | None = Field(None, description="Human-readable lab name")
369
+ available_locations: list[str] | None = Field(None, description="List of valid locations")
370
+
371
+
372
+ class PrinterUpdateRequest(BaseModel):
373
+ """Request model for updating printer settings."""
374
+
375
+ printer_name: str | None = Field(None, description="User-friendly display name")
376
+ lab_location: str | None = Field(None, description="Location within the lab")
377
+ notes: str | None = Field(None, description="Optional notes")
378
+ label_zpl_styles: list[str] | None = Field(None, description="Allowed ZPL styles")
379
+ default_label_style: str | None = Field(
380
+ None, description="Default label style to use when none specified"
381
+ )
382
+
383
+
384
+ @router.patch("/labs/{lab}", response_model=LabInfo)
385
+ async def update_lab(request: Request, lab: str, update: LabUpdateRequest) -> LabInfo:
386
+ """Update lab settings (lab_name, available_locations)."""
387
+ zp = request.app.state.zp
388
+ labs = zp.printers.get("labs", {})
389
+
390
+ if lab not in labs:
391
+ raise HTTPException(status_code=404, detail=f"Lab '{lab}' not found")
392
+
393
+ lab_data = labs[lab]
394
+
395
+ if update.lab_name is not None:
396
+ lab_data["lab_name"] = update.lab_name
397
+ if update.available_locations is not None:
398
+ lab_data["available_locations"] = update.available_locations
399
+
400
+ # Save changes
401
+ zp.save_printer_json(zp.printers_filename, relative=False)
402
+
403
+ # Return updated lab info
404
+ return await get_lab(request, lab)
405
+
406
+
407
+ @router.patch("/labs/{lab}/printers/{printer_id}")
408
+ async def update_printer(
409
+ request: Request, lab: str, printer_id: str, update: PrinterUpdateRequest
410
+ ) -> PrinterInfo:
411
+ """Update printer settings (printer_name, lab_location, notes)."""
412
+ zp = request.app.state.zp
413
+ labs = zp.printers.get("labs", {})
414
+
415
+ if lab not in labs:
416
+ raise HTTPException(status_code=404, detail=f"Lab '{lab}' not found")
417
+
418
+ lab_printers = labs[lab].get("printers", {})
419
+ if printer_id not in lab_printers:
420
+ raise HTTPException(
421
+ status_code=404, detail=f"Printer '{printer_id}' not found in lab '{lab}'"
422
+ )
423
+
424
+ printer_data = lab_printers[printer_id]
425
+
426
+ if update.printer_name is not None:
427
+ printer_data["printer_name"] = update.printer_name if update.printer_name else None
428
+ if update.lab_location is not None:
429
+ printer_data["lab_location"] = update.lab_location if update.lab_location else None
430
+ if update.notes is not None:
431
+ printer_data["notes"] = update.notes
432
+ if update.label_zpl_styles is not None:
433
+ printer_data["label_zpl_styles"] = update.label_zpl_styles
434
+ if update.default_label_style is not None:
435
+ # Validate that the style exists in label_zpl_styles (if it's not empty string)
436
+ if update.default_label_style and update.default_label_style not in printer_data.get(
437
+ "label_zpl_styles", []
438
+ ):
439
+ raise HTTPException(
440
+ status_code=400,
441
+ detail=f"Default label style '{update.default_label_style}' must be one of: {printer_data.get('label_zpl_styles', [])}",
442
+ )
443
+ printer_data["default_label_style"] = (
444
+ update.default_label_style if update.default_label_style else None
445
+ )
163
446
 
447
+ # Save changes
448
+ zp.save_printer_json(zp.printers_filename, relative=False)
449
+
450
+ return PrinterInfo(
451
+ id=printer_id,
452
+ ip_address=printer_data.get("ip_address", ""),
453
+ printer_name=printer_data.get("printer_name"),
454
+ lab_location=printer_data.get("lab_location"),
455
+ manufacturer=printer_data.get("manufacturer", "zebra"),
456
+ model=printer_data.get("model", ""),
457
+ serial=printer_data.get("serial", ""),
458
+ label_zpl_styles=printer_data.get("label_zpl_styles", []),
459
+ default_label_style=printer_data.get("default_label_style"),
460
+ print_method=printer_data.get("print_method", ""),
461
+ notes=printer_data.get("notes", ""),
462
+ )