Skip to content

Commit 416a653

Browse files
committed
Merge branch 'codex/zebra-day-3-bloom-cutover'
2 parents 319de07 + 3e3cc97 commit 416a653

23 files changed

Lines changed: 794 additions & 333 deletions

bloom_lims/cli/server.py

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@
99
from cli_core_yo.registry import CommandRegistry
1010
from cli_core_yo.spec import CliSpec
1111

12+
import json
1213
import os
1314
import subprocess
1415
import sys
1516
import time
1617
from pathlib import Path
1718

1819
import typer
19-
from cli_core_yo.certs import ensure_certs
20+
from cli_core_yo.certs import resolve_https_certs, shared_dayhoff_certs_dir
2021
from cli_core_yo.server import (
2122
display_host,
2223
latest_log,
@@ -40,6 +41,7 @@
4041

4142
PROJECT_ROOT = Path(__file__).resolve().parents[2]
4243
TAPDB_LOG_DIR = Path.home() / ".config" / "tapdb" / "logs"
44+
SERVER_META_FILE = "server-meta.json"
4345

4446

4547
class LogService(str, Enum):
@@ -66,7 +68,12 @@ def _pid_file() -> Path:
6668
return _state_dir() / "server.pid"
6769

6870

71+
def _runtime_meta_file() -> Path:
72+
return _state_dir() / SERVER_META_FILE
73+
74+
6975
def _ensure_dir() -> None:
76+
_state_dir().mkdir(parents=True, exist_ok=True)
7077
_log_dir().mkdir(parents=True, exist_ok=True)
7178

7279

@@ -83,7 +90,7 @@ def server_status_label() -> str:
8390
pid, _ = active_server_pid()
8491
if pid is None:
8592
return "Stopped"
86-
return f"Running (PID {pid})"
93+
return f"Running ({_runtime_scheme().upper()}, PID {pid})"
8794

8895

8996
def _runtime_host_and_port(default_port: int, default_host: str) -> tuple[str, int]:
@@ -98,6 +105,43 @@ def _runtime_host_and_port(default_port: int, default_host: str) -> tuple[str, i
98105
return host, port
99106

100107

108+
def _deployment_shared_certs_dir() -> Path:
109+
from bloom_lims.config import _resolve_deployment_code
110+
111+
return shared_dayhoff_certs_dir(_resolve_deployment_code())
112+
113+
114+
def _write_runtime_meta(*, ssl_enabled: bool) -> None:
115+
_runtime_meta_file().write_text(
116+
json.dumps({"ssl_enabled": ssl_enabled}, sort_keys=True),
117+
encoding="utf-8",
118+
)
119+
120+
121+
def _read_runtime_meta() -> dict[str, object]:
122+
meta_file = _runtime_meta_file()
123+
if not meta_file.exists():
124+
return {}
125+
try:
126+
payload = json.loads(meta_file.read_text(encoding="utf-8"))
127+
except Exception:
128+
return {}
129+
return payload if isinstance(payload, dict) else {}
130+
131+
132+
def _clear_runtime_meta() -> None:
133+
_runtime_meta_file().unlink(missing_ok=True)
134+
135+
136+
def _runtime_scheme() -> str:
137+
meta = _read_runtime_meta()
138+
if str(meta.get("ssl_enabled")).lower() in {"false", "0", "no"}:
139+
return "http"
140+
if str(meta.get("ssl_enabled")).lower() in {"true", "1", "yes"}:
141+
return "https"
142+
return "https"
143+
144+
101145
@server_app.command("start")
102146
def start(
103147
port: int = typer.Option(
@@ -113,13 +157,33 @@ def start(
113157
"-b/-f",
114158
help="Run in background",
115159
),
160+
ssl: bool = typer.Option(
161+
True,
162+
"--ssl/--no-ssl",
163+
help="Serve over HTTPS with deployment-scoped certs",
164+
),
165+
cert: str | None = typer.Option(None, "--cert", help="TLS certificate file"),
166+
key: str | None = typer.Option(None, "--key", help="TLS private key file"),
116167
) -> None:
117168
"""Start the BLOOM web UI."""
118169
_ensure_dir()
119170
host, port = _runtime_host_and_port(port, host)
120171
shown_host = display_host(host)
121-
protocol = "https"
122-
cert_file, key_file = ensure_certs(PROJECT_ROOT / "certs")
172+
if not ssl and (cert or key):
173+
console.print("[red]✗[/red] --cert and --key require HTTPS; omit them with --no-ssl")
174+
raise typer.Exit(1)
175+
176+
protocol = "https" if ssl else "http"
177+
cert_file = key_file = None
178+
if ssl:
179+
resolved = resolve_https_certs(
180+
cert_path=cert,
181+
key_path=key,
182+
shared_certs_dir=_deployment_shared_certs_dir(),
183+
fallback_certs_dir=PROJECT_ROOT / "certs",
184+
)
185+
cert_file = resolved.cert_path
186+
key_file = resolved.key_path
123187

124188
try:
125189
settings = get_settings()
@@ -184,10 +248,12 @@ def start(
184248
]
185249
if reload:
186250
cmd.append("--reload")
187-
cmd.extend(["--ssl-keyfile", str(key_file), "--ssl-certfile", str(cert_file)])
251+
if ssl and cert_file and key_file:
252+
cmd.extend(["--ssl-keyfile", str(key_file), "--ssl-certfile", str(cert_file)])
188253

189254
env = os.environ.copy()
190255
env["PYTHONUNBUFFERED"] = "1"
256+
_write_runtime_meta(ssl_enabled=ssl)
191257

192258
if background:
193259
log_file = new_log_path(_log_dir())
@@ -202,6 +268,7 @@ def start(
202268
)
203269
time.sleep(2)
204270
if proc.poll() is not None:
271+
_clear_runtime_meta()
205272
console.print("[red]✗[/red] Server failed to start. Check logs:")
206273
console.print(f" [dim]{log_file}[/dim]")
207274
raise typer.Exit(1)
@@ -221,13 +288,17 @@ def start(
221288
subprocess.run(cmd, env=env, cwd=PROJECT_ROOT)
222289
except KeyboardInterrupt:
223290
console.print("\n[yellow]⚠[/yellow] Server stopped")
291+
except Exception:
292+
_clear_runtime_meta()
293+
raise
224294

225295

226296
@server_app.command("stop")
227297
def stop() -> None:
228298
"""Stop the BLOOM web UI."""
229299
stopped, msg = stop_pid(_pid_file())
230300
if stopped:
301+
_clear_runtime_meta()
231302
console.print(f"[green]✓[/green] {msg}")
232303
elif "Permission" in msg:
233304
console.print(f"[red]✗[/red] {msg}")
@@ -245,7 +316,7 @@ def status() -> None:
245316
log_file = _latest_server_log()
246317
if pid:
247318
console.print(f"[green]●[/green] Server is [green]running[/green] (PID {pid})")
248-
console.print(f" URL: [cyan]https://{shown_host}:{port}[/cyan]")
319+
console.print(f" URL: [cyan]{_runtime_scheme()}://{shown_host}:{port}[/cyan]")
249320
if log_file:
250321
console.print(f" Logs: [dim]{log_file}[/dim]")
251322
return

bloom_lims/config.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,27 @@ def validate_enabled_contract(self) -> "DeweySettings":
482482
return self
483483

484484

485+
class ZebraDaySettings(BaseModel):
486+
"""zebra_day integration settings."""
487+
488+
base_url: str = Field(default="", description="zebra_day API base URL")
489+
token: str = Field(default="", description="zebra_day internal API bearer token")
490+
timeout_seconds: int = Field(default=10, description="zebra_day API timeout seconds")
491+
verify_ssl: bool = Field(
492+
default=True, description="Verify zebra_day TLS certificates"
493+
)
494+
495+
@field_validator("base_url")
496+
@classmethod
497+
def validate_base_url(cls, value: str) -> str:
498+
normalized = str(value or "").strip()
499+
if not normalized:
500+
return ""
501+
if not normalized.startswith(("https://", "http://")):
502+
raise ValueError("zebra_day.base_url must use an absolute http:// or https:// URL")
503+
return normalized.rstrip("/")
504+
505+
485506
class LoggingSettings(BaseModel):
486507
"""Logging configuration."""
487508

@@ -640,6 +661,7 @@ class BloomSettings(BaseSettings):
640661
auth: AuthSettings = Field(default_factory=AuthSettings)
641662
atlas: AtlasSettings = Field(default_factory=AtlasSettings)
642663
dewey: DeweySettings = Field(default_factory=DeweySettings)
664+
zebra_day: ZebraDaySettings = Field(default_factory=ZebraDaySettings)
643665
aws: AWSSettings = Field(default_factory=AWSSettings)
644666
ui: UISettings = Field(default_factory=UISettings)
645667
deployment: DeploymentSettings = Field(default_factory=DeploymentSettings)

bloom_lims/core/action_execution.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -319,8 +319,11 @@ def _build_action_ds(
319319
action_ds["curr_user_id"] = actor_user_id
320320
action_ds["lab"] = prefs.get("print_lab", "BLOOM")
321321
action_ds["printer_name"] = prefs.get("printer_name", "")
322-
action_ds["label_style"] = prefs.get("label_style", "")
323-
action_ds["label_zpl_style"] = prefs.get("label_style", "")
322+
resolved_label_style = prefs.get("label_zpl_style", "") or prefs.get(
323+
"label_style", ""
324+
)
325+
action_ds["label_style"] = resolved_label_style
326+
action_ds["label_zpl_style"] = resolved_label_style
324327
action_ds["alt_a"] = prefs.get("alt_a", "")
325328
action_ds["alt_b"] = prefs.get("alt_b", "")
326329
action_ds["alt_c"] = prefs.get("alt_c", "")

bloom_lims/docs/printer_config.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
11
# Zebra Printer Configuration
2-
Bloom relies on [zebra_day](https://github.com/Daylily-Informatics/zebra_day) to administer the lab printer fleet and broker print requests. Please see that repository for printer-specific setup details. The notes below are a Bloom-focused quick reference.
2+
Bloom relies on [zebra_day](https://github.com/Daylily-Informatics/zebra_day) as the authoritative printer-fleet service. Bloom fetches shared printer, template, and label-profile state from the zebra_day API and submits print jobs back to zebra_day for delivery. Please see that repository for printer-specific setup details. The notes below are a Bloom-focused quick reference.
33

44

55
## Printer Setup
6-
* From the venv you are running Bloom from, the `zebra_day` package should be pre-installed. To start the admin web interface, from your venv, run `zday_start`. This will start the admin web interface on port 8118 on the machine you are running Bloom from.
6+
* Bloom does not start or manage zebra_day. Configure the running zebra_day service with `bloom.zebra_day.base_url` and `bloom.zebra_day.token` or the equivalent `BLOOM_ZEBRA_DAY__*` environment variables.
77

88
### Detect Printers On Your Local Network
99
_this MUST be done at least once when setting up a new bloom install_ && _done again when adding new printers_
10-
* `zebra_day` can scan the local network (barring firewal rules blocking this) and update the venv printer config file with detected printers. Open the `Scan Network For Zebra Printers` page @ `http://localhost:8118/build_new_printers_config_json`. You IP prefix should be auto detected, else enter the first three sections (ie `192.168.1`). Enter a few character 'Lab Code' (only alphanumeric, no whitespace, etc.), this will be the code under which detected printers are added to the json config. The scan might take a few minutes to complete.
10+
* Use the zebra_day admin UI to discover printers, assign printer IDs, and manage default label profiles. Bloom reads those shared records remotely; it no longer rebuilds local printer JSON.
1111

1212
![Printer Scan](./imgs/bc_scan.png)
1313

14-
* The scan will return all detected printers. The `Lab Code` + `Printer Name` will uniquely identify each printer config. There are several label styles included with `zebra_day`, you may specify a label style with each print request. Make sure the label stock matches the label style. See `zebra_day` docs for more information on label styles. I will use the common `2x1in` label style for the rest of this document. From the `zebra_day` admin web interface, you can print a test label to verify the printer is working.
15-
16-
* The printers visible in the Scan Report will be the printers available in the bloom UI. NOTE- you may change names of the printers, see the zebra_day docs for more information.
14+
* The printers visible in zebra_day are the printers available in the Bloom UI. Bloom stores the selected zebra_day `printer_id` as the user preference value and uses zebra_day label-profile names for `label_zpl_style`.
1715

1816
![Printer Test](./imgs/printer_fleet_status.png)
1917

bloom_lims/domain/base.py

Lines changed: 33 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import os
1313
import re
1414

15-
import zebra_day.print_mgr as zdpm
1615
from daylily_tapdb import MissingSeededTemplateError
1716
from sqlalchemy import (
1817
DateTime,
@@ -51,8 +50,12 @@ def __init__(
5150
self.logger = logging.getLogger(__name__ + ".BloomObj")
5251
self.logger.debug("Instantiating BloomObj")
5352

54-
# Zebra Day Print Manager
55-
self.zpld = zdpm.zpl()
53+
self.printer_labs = []
54+
self.selected_lab = ""
55+
self.site_printers = []
56+
self.printer_options = []
57+
self.zpl_label_styles = []
58+
self.selected_label_style = "tube_2inX1in"
5659
if cfg_printers:
5760
self._config_printers()
5861

@@ -338,47 +341,31 @@ def _fetch_fedex_tracking_ops_meta(self, tracking_number):
338341
return [data]
339342
return [dict(data)]
340343

341-
def _rebuild_printer_json(self, lab="BLOOM"):
342-
self.zpld.probe_zebra_printers_add_to_printers_json(lab=lab)
343-
self.zpld.save_printer_json(self.zpld.printers_filename.split("zebra_day")[-1])
344-
self._config_printers()
345-
346344
def _config_printers(self):
347-
if len(self.zpld.printers["labs"].keys()) == 0:
348-
self.logger.warning(
349-
"No printers found, attempting to rebuild printer json\n\n"
350-
)
351-
self.logger.warning(
352-
'This may take a few minutes, lab code will be set to "BLOOM" ... please sit tight...\n\n'
353-
)
354-
self._rebuild_printer_json()
355-
356-
self.printer_labs = self.zpld.printers["labs"].keys()
357-
self.selected_lab = sorted(self.printer_labs)[0]
358-
self.site_printers = self.zpld.printers["labs"][self.selected_lab].keys()
359-
_zpl_label_styles = []
360-
for zpl_f in os.listdir(
361-
os.path.dirname(self.zpld.printers_filename) + "/label_styles/"
362-
):
363-
if zpl_f.endswith(".zpl"):
364-
_zpl_label_styles.append(zpl_f.removesuffix(".zpl"))
365-
self.zpl_label_styles = sorted(_zpl_label_styles)
366-
self.selected_label_style = "tube_2inX1in"
345+
from bloom_lims.integrations.zebra_day import ZebraDayService
346+
347+
printer_info = ZebraDayService().build_printer_preferences()
348+
self.printer_labs = list(printer_info.get("print_lab", []))
349+
self.selected_lab = str(printer_info.get("selected_lab", "") or "")
350+
self.printer_options = list(printer_info.get("printer_options", []))
351+
self.site_printers = [item["value"] for item in self.printer_options]
352+
self.zpl_label_styles = list(printer_info.get("label_zpl_style", []))
353+
self.selected_label_style = (
354+
self.zpl_label_styles[0] if self.zpl_label_styles else "tube_2inX1in"
355+
)
367356

368357
def set_printers_lab(self, lab):
369358
self.selected_lab = lab
370359

371360
def get_lab_printers(self, lab):
372-
self.selected_lab = lab
373-
try:
374-
self.site_printers = self.zpld.printers["labs"][self.selected_lab].keys()
375-
except Exception as e:
376-
self.logger.error(f"Error getting printers for lab {lab}")
377-
self.logger.error(e)
378-
self.logger.error(
379-
"\n\n\nAttempting to rebuild printer json !!! THIS WILL TAKE TIME !!!\n\n\n"
380-
)
381-
self._rebuild_printer_json()
361+
from bloom_lims.integrations.zebra_day import ZebraDayService
362+
363+
printer_info = ZebraDayService().build_printer_preferences(lab)
364+
self.selected_lab = str(printer_info.get("selected_lab", "") or "")
365+
self.printer_labs = list(printer_info.get("print_lab", []))
366+
self.printer_options = list(printer_info.get("printer_options", []))
367+
self.site_printers = [item["value"] for item in self.printer_options]
368+
self.zpl_label_styles = list(printer_info.get("label_zpl_style", []))
382369

383370
def print_label(
384371
self,
@@ -394,18 +381,19 @@ def print_label(
394381
alt_f="",
395382
print_n=1,
396383
):
397-
self.zpld.print_zpl(
384+
from bloom_lims.integrations.zebra_day import ZebraDayService
385+
386+
ZebraDayService().submit_print_job(
398387
lab=lab,
399-
printer_name=printer_name,
400-
uid_barcode=euid,
388+
printer_id=printer_name,
389+
euid=euid,
401390
alt_a=alt_a,
402391
alt_b=alt_b,
403392
alt_c=alt_c,
404393
alt_d=alt_d,
405394
alt_e=alt_e,
406395
alt_f=alt_f,
407396
label_zpl_style=label_zpl_style,
408-
client_ip="pkg",
409397
print_n=print_n,
410398
)
411399

@@ -1603,7 +1591,9 @@ def do_action_print_barcode_label(self, euid, action_ds={}):
16031591

16041592
lab = action_ds.get("lab", "")
16051593
printer_name = action_ds.get("printer_name", "")
1606-
label_zpl_style = action_ds.get("label_style", "")
1594+
label_zpl_style = action_ds.get("label_zpl_style", "") or action_ds.get(
1595+
"label_style", ""
1596+
)
16071597
alt_a = (
16081598
action_ds.get("alt_a", "")
16091599
if not PGLOBAL

bloom_lims/etc/bloom-config-template.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,15 @@ dewey:
163163
timeout_seconds: 10
164164
verify_ssl: true
165165

166+
# -----------------------------------------------------------------------------
167+
# zebra_day Integration
168+
# -----------------------------------------------------------------------------
169+
zebra_day:
170+
base_url: "" # e.g., https://zebra-day.example.org
171+
token: "" # Bearer token used for zebra_day shared printer/config APIs
172+
timeout_seconds: 10
173+
verify_ssl: true
174+
166175
# -----------------------------------------------------------------------------
167176
# Logging Configuration
168177
# -----------------------------------------------------------------------------

0 commit comments

Comments
 (0)