zebra-day 0.0.37__py3-none-any.whl → 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. zebra_day/__init__.py +35 -0
  2. zebra_day/bin/__init__.py +0 -0
  3. zebra_day/cli/__init__.py +240 -0
  4. zebra_day/cli/cognito.py +121 -0
  5. zebra_day/cli/gui.py +338 -0
  6. zebra_day/cli/printer.py +168 -0
  7. zebra_day/cli/template.py +176 -0
  8. zebra_day/cmd_mgr.py +35 -0
  9. zebra_day/etc/Monoid-Regular-HalfTight-Dollar-0-1-l.ttf +0 -0
  10. zebra_day/etc/label_styles/blank.zpl +0 -0
  11. zebra_day/etc/label_styles/cornersStripOf4Squares_1inX1in.zpl +55 -0
  12. zebra_day/etc/label_styles/corners_1inX2in.zpl +28 -0
  13. zebra_day/etc/label_styles/corners_20cmX30cm.zpl +6 -0
  14. zebra_day/etc/label_styles/corners_smallTube.zpl +7 -0
  15. zebra_day/etc/label_styles/corners_unspecifiedDimensions.zpl +15 -0
  16. zebra_day/etc/label_styles/generic_2inX1in.zpl +21 -0
  17. zebra_day/etc/label_styles/plate_1inX0.25in.zpl +9 -0
  18. zebra_day/etc/label_styles/plate_1inX0.25inHD.zpl +9 -0
  19. zebra_day/etc/label_styles/smallTubeWdotHD_prod.zpl +8 -0
  20. zebra_day/etc/label_styles/smallTubeWdot_corners.zpl +7 -0
  21. zebra_day/etc/label_styles/smallTubeWdot_prod.zpl +8 -0
  22. zebra_day/etc/label_styles/smallTubeWdot_prodAlt1.zpl +6 -0
  23. zebra_day/etc/label_styles/smallTubeWdot_prodAlt1b.zpl +3 -0
  24. zebra_day/etc/label_styles/smallTubeWdot_prodV2.zpl +8 -0
  25. zebra_day/etc/label_styles/smallTubeWdot_reagent.zpl +29 -0
  26. zebra_day/etc/label_styles/stripOf4Squares_1inX1in.zpl +32 -0
  27. zebra_day/etc/label_styles/test_800dX800dCoordinateArray.zpl +1 -0
  28. zebra_day/etc/label_styles/tmps/.hold +0 -0
  29. zebra_day/etc/label_styles/tmps/tmp_zpl_templates.here +0 -0
  30. zebra_day/etc/label_styles/tube_20mmX30mmA.zpl +7 -0
  31. zebra_day/etc/label_styles/tube_2inX0.3in.zpl +15 -0
  32. zebra_day/etc/label_styles/tube_2inX0.5in.zpl +15 -0
  33. zebra_day/etc/label_styles/tube_2inX0.5inHD.zpl +15 -0
  34. zebra_day/etc/label_styles/tube_2inX1in.zpl +25 -0
  35. zebra_day/etc/label_styles/tube_2inX1inHD.zpl +22 -0
  36. zebra_day/etc/label_styles/tube_2inX1inHDv3.zpl +21 -0
  37. zebra_day/etc/old_printer_config/.hold +0 -0
  38. zebra_day/etc/old_printer_config/2026-02-01_01:50:25.022846_printer_config.json +1 -0
  39. zebra_day/etc/old_printer_config/2026-02-01_01:50:25.033657_printer_config.json +1 -0
  40. zebra_day/etc/old_printer_config/2026-02-01_01:50:25.039597_printer_config.json +3 -0
  41. zebra_day/etc/old_printer_config/2026-02-01_01:50:25.047295_printer_config.json +1 -0
  42. zebra_day/etc/old_printer_config/2026-02-01_01:50:25.055804_printer_config.json +1 -0
  43. zebra_day/etc/old_printer_config/2026-02-01_01:50:25.061337_printer_config.json +3 -0
  44. zebra_day/etc/old_printer_config/2026-02-01_01:51:24.073326_printer_config.json +1 -0
  45. zebra_day/etc/old_printer_config/2026-02-01_01:51:24.081950_printer_config.json +1 -0
  46. zebra_day/etc/old_printer_config/2026-02-01_01:51:24.088251_printer_config.json +3 -0
  47. zebra_day/etc/old_printer_config/2026-02-01_01:51:24.096501_printer_config.json +1 -0
  48. zebra_day/etc/old_printer_config/2026-02-01_01:51:24.104767_printer_config.json +1 -0
  49. zebra_day/etc/old_printer_config/2026-02-01_01:51:24.110364_printer_config.json +3 -0
  50. zebra_day/etc/old_printer_config/2026-02-01_01:51:24.118239_printer_config.json +1 -0
  51. zebra_day/etc/old_printer_config/2026-02-01_01:51:24.125950_printer_config.json +1 -0
  52. zebra_day/etc/old_printer_config/2026-02-01_01:51:24.349866_printer_config.json +1 -0
  53. zebra_day/etc/old_printer_config/2026-02-01_01:51:24.361085_printer_config.json +3 -0
  54. zebra_day/etc/old_printer_config/2026-02-01_01:51:24.558323_printer_config.json +1 -0
  55. zebra_day/etc/old_printer_config/2026-02-01_01:51:24.565756_printer_config.json +3 -0
  56. zebra_day/etc/old_printer_config/2026-02-01_01:51:29.739070_printer_config.json +16 -0
  57. zebra_day/etc/old_printer_config/2026-02-01_01:51:29.753796_printer_config.json +1 -0
  58. zebra_day/etc/old_printer_config/2026-02-01_01:51:29.760201_printer_config.json +3 -0
  59. zebra_day/etc/old_printer_config/2026-02-01_01:51:29.768747_printer_config.json +1 -0
  60. zebra_day/etc/old_printer_config/2026-02-01_01:51:29.775312_printer_config.json +3 -0
  61. zebra_day/etc/old_printer_config/2026-02-01_01:51:29.782533_printer_config.json +1 -0
  62. zebra_day/etc/old_printer_config/2026-02-01_01:51:29.789287_printer_config.json +1 -0
  63. zebra_day/etc/old_printer_config/2026-02-01_01:51:29.794230_printer_config.json +3 -0
  64. zebra_day/etc/old_printer_config/2026-02-01_01:51:29.800021_printer_config.json +5 -0
  65. zebra_day/etc/printer_config.json +4 -0
  66. zebra_day/etc/printer_config.template.json +24 -0
  67. zebra_day/etc/tmp_printers0.json +5 -0
  68. zebra_day/etc/tmp_printers120.json +10 -0
  69. zebra_day/etc/tmp_printers145.json +10 -0
  70. zebra_day/etc/tmp_printers207.json +10 -0
  71. zebra_day/etc/tmp_printers374.json +5 -0
  72. zebra_day/etc/tmp_printers383.json +5 -0
  73. zebra_day/etc/tmp_printers450.json +5 -0
  74. zebra_day/etc/tmp_printers469.json +10 -0
  75. zebra_day/etc/tmp_printers485.json +10 -0
  76. zebra_day/etc/tmp_printers504.json +5 -0
  77. zebra_day/etc/tmp_printers531.json +10 -0
  78. zebra_day/etc/tmp_printers540.json +10 -0
  79. zebra_day/etc/tmp_printers542.json +10 -0
  80. zebra_day/etc/tmp_printers552.json +10 -0
  81. zebra_day/etc/tmp_printers608.json +5 -0
  82. zebra_day/etc/tmp_printers657.json +5 -0
  83. zebra_day/etc/tmp_printers715.json +10 -0
  84. zebra_day/etc/tmp_printers838.json +5 -0
  85. zebra_day/etc/tmp_printers839.json +5 -0
  86. zebra_day/etc/tmp_printers933.json +5 -0
  87. zebra_day/etc/tmp_printers957.json +5 -0
  88. zebra_day/etc/tmp_printers972.json +10 -0
  89. zebra_day/exceptions.py +88 -0
  90. zebra_day/files/.hold +0 -0
  91. zebra_day/files/blank_preview.png +0 -0
  92. zebra_day/files/corners_20cmX30cm_preview.png +0 -0
  93. zebra_day/files/generic_2inX1in_preview.png +0 -0
  94. zebra_day/files/hold +0 -0
  95. zebra_day/files/test_png_12020.png +0 -0
  96. zebra_day/files/test_png_12352.png +0 -0
  97. zebra_day/files/test_png_15472.png +0 -0
  98. zebra_day/files/test_png_17696.png +0 -0
  99. zebra_day/files/test_png_23477.png +0 -0
  100. zebra_day/files/test_png_24493.png +0 -0
  101. zebra_day/files/test_png_28157.png +0 -0
  102. zebra_day/files/test_png_30069.png +0 -0
  103. zebra_day/files/test_png_35832.png +0 -0
  104. zebra_day/files/test_png_36400.png +0 -0
  105. zebra_day/files/test_png_40816.png +0 -0
  106. zebra_day/files/test_png_47791.png +0 -0
  107. zebra_day/files/test_png_47799.png +0 -0
  108. zebra_day/files/test_png_49564.png +0 -0
  109. zebra_day/files/test_png_53848.png +0 -0
  110. zebra_day/files/test_png_55588.png +0 -0
  111. zebra_day/files/test_png_58809.png +0 -0
  112. zebra_day/files/test_png_62542.png +0 -0
  113. zebra_day/files/test_png_67242.png +0 -0
  114. zebra_day/files/test_png_89893.png +0 -0
  115. zebra_day/files/test_png_91597.png +0 -0
  116. zebra_day/files/test_png_93633.png +0 -0
  117. zebra_day/files/tmpbjo3k7q1.png +0 -0
  118. zebra_day/files/tmpigtr4pwy.png +0 -0
  119. zebra_day/files/tube_20mmX30mmA_preview.png +0 -0
  120. zebra_day/files/zpl_label_tube_2inX1in_2026-02-01_01:51:24.370964.png +0 -0
  121. zebra_day/logging_config.py +74 -0
  122. zebra_day/logs/.hold +0 -0
  123. zebra_day/logs/print_requests.log +2 -0
  124. zebra_day/paths.py +143 -0
  125. zebra_day/print_mgr.py +557 -117
  126. zebra_day/static/datschund.css +140 -0
  127. zebra_day/static/datschund.png +0 -0
  128. zebra_day/static/daylily.png +0 -0
  129. zebra_day/static/favicon.svg +20 -0
  130. zebra_day/static/general.css +99 -0
  131. zebra_day/static/js/zebra_modern.js +172 -0
  132. zebra_day/static/lsmc.css +354 -0
  133. zebra_day/static/moon.jpeg +0 -0
  134. zebra_day/static/oakland.css +197 -0
  135. zebra_day/static/petrichor.css +150 -0
  136. zebra_day/static/popday_daylily.css +140 -0
  137. zebra_day/static/style.css +183 -0
  138. zebra_day/static/triangles.css +122 -0
  139. zebra_day/static/tron.css +277 -0
  140. zebra_day/static/zebra_modern.css +771 -0
  141. zebra_day/static/zebras.css +176 -0
  142. zebra_day/templates/modern/base.html +98 -0
  143. zebra_day/templates/modern/config.html +141 -0
  144. zebra_day/templates/modern/config_backups.html +59 -0
  145. zebra_day/templates/modern/config_editor.html +95 -0
  146. zebra_day/templates/modern/config_new.html +93 -0
  147. zebra_day/templates/modern/dashboard.html +160 -0
  148. zebra_day/templates/modern/print_request.html +145 -0
  149. zebra_day/templates/modern/print_result.html +88 -0
  150. zebra_day/templates/modern/printer_detail.html +244 -0
  151. zebra_day/templates/modern/printers.html +144 -0
  152. zebra_day/templates/modern/save_result.html +46 -0
  153. zebra_day/templates/modern/template_editor.html +175 -0
  154. zebra_day/templates/modern/templates.html +122 -0
  155. zebra_day/web/__init__.py +9 -0
  156. zebra_day/web/app.py +248 -0
  157. zebra_day/web/auth.py +172 -0
  158. zebra_day/web/middleware.py +159 -0
  159. zebra_day/web/routers/__init__.py +2 -0
  160. zebra_day/web/routers/api.py +313 -0
  161. zebra_day/web/routers/ui.py +636 -0
  162. zebra_day/zpl_renderer.py +273 -0
  163. zebra_day-2.0.0.dist-info/METADATA +847 -0
  164. zebra_day-2.0.0.dist-info/RECORD +168 -0
  165. {zebra_day-0.0.37.dist-info → zebra_day-2.0.0.dist-info}/WHEEL +1 -1
  166. zebra_day-2.0.0.dist-info/entry_points.txt +4 -0
  167. zebra_day/bin/scan_for_networed_zebra_printers.py +0 -23
  168. zebra_day/bin/te.py +0 -905
  169. zebra_day/bin/zserve.py +0 -620
  170. zebra_day-0.0.37.dist-info/METADATA +0 -1177
  171. zebra_day-0.0.37.dist-info/RECORD +0 -10
  172. {zebra_day-0.0.37.dist-info → zebra_day-2.0.0.dist-info/licenses}/LICENSE +0 -0
  173. {zebra_day-0.0.37.dist-info → zebra_day-2.0.0.dist-info}/top_level.txt +0 -0
zebra_day/web/app.py ADDED
@@ -0,0 +1,248 @@
1
+ """
2
+ FastAPI application factory for zebra_day.
3
+
4
+ This module provides the main FastAPI application for the zebra_day web interface.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ import subprocess
10
+ from pathlib import Path
11
+ from typing import Literal, Optional
12
+
13
+ from fastapi import FastAPI, Request
14
+ from fastapi.staticfiles import StaticFiles
15
+ from fastapi.templating import Jinja2Templates
16
+ from importlib.resources import files
17
+
18
+ from zebra_day.logging_config import get_logger
19
+ from zebra_day import paths as xdg
20
+ from zebra_day.web.middleware import RequestLoggingMiddleware, print_rate_limiter
21
+
22
+ _log = get_logger(__name__)
23
+
24
+ # Package paths
25
+ _PKG_PATH = Path(str(files("zebra_day")))
26
+ _STATIC_PATH = _PKG_PATH / "static"
27
+ _TEMPLATES_PATH = _PKG_PATH / "templates"
28
+
29
+
30
+ def get_local_ip() -> str:
31
+ """Get the local IP address of this machine."""
32
+ ipcmd = r"""(ip addr show | grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' | grep -Eo '([0-9]*\.){3}[0-9]*' | grep -v '127.0.0.1' || ifconfig | grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' | grep -Eo '([0-9]*\.){3}[0-9]*' | grep -v '127.0.0.1') 2>/dev/null"""
33
+ result = subprocess.run(ipcmd, shell=True, capture_output=True, text=True)
34
+ lines = result.stdout.strip().split("\n")
35
+ return lines[0] if lines and lines[0] else "127.0.0.1"
36
+
37
+
38
+ def create_app(
39
+ *,
40
+ debug: bool = False,
41
+ css_theme: str = "lsmc.css",
42
+ auth: Optional[Literal["none", "cognito"]] = None,
43
+ ) -> FastAPI:
44
+ """
45
+ Create and configure the FastAPI application.
46
+
47
+ Args:
48
+ debug: Enable debug mode
49
+ css_theme: Default CSS theme file name
50
+ auth: Authentication mode - "none" (public) or "cognito" (AWS Cognito).
51
+ If None, reads from ZEBRA_DAY_AUTH_MODE env var (defaults to "none").
52
+
53
+ Returns:
54
+ Configured FastAPI application
55
+ """
56
+ # Get auth mode from parameter or environment variable
57
+ if auth is None:
58
+ auth = os.environ.get("ZEBRA_DAY_AUTH_MODE", "none") # type: ignore[assignment]
59
+
60
+ # Validate auth parameter
61
+ if auth not in ("none", "cognito"):
62
+ raise ValueError(f"Invalid auth mode: {auth!r}. Must be 'none' or 'cognito'.")
63
+
64
+ app = FastAPI(
65
+ title="Zebra Day",
66
+ description="Zebra printer fleet management and label printing",
67
+ version="0.5.0",
68
+ debug=debug,
69
+ )
70
+
71
+ # Add request logging middleware
72
+ app.add_middleware(RequestLoggingMiddleware)
73
+
74
+ # Configure authentication if enabled
75
+ if auth == "cognito":
76
+ from zebra_day.web.auth import CognitoAuthMiddleware, setup_cognito_auth
77
+
78
+ cognito_auth = setup_cognito_auth(app)
79
+ app.add_middleware(CognitoAuthMiddleware, cognito_auth=cognito_auth)
80
+ app.state.cognito_auth = cognito_auth
81
+ app.state.auth_mode = "cognito"
82
+ _log.info("Cognito authentication middleware enabled")
83
+ else:
84
+ app.state.auth_mode = "none"
85
+ _log.info("Authentication disabled (auth=none)")
86
+
87
+ # Store rate limiter in app state for use in endpoints
88
+ app.state.print_rate_limiter = print_rate_limiter
89
+
90
+ # Store app state
91
+ app.state.css_theme = css_theme
92
+ app.state.local_ip = get_local_ip()
93
+ app.state.pkg_path = _PKG_PATH
94
+
95
+ # Mount static files
96
+ app.mount("/static", StaticFiles(directory=str(_STATIC_PATH)), name="static")
97
+
98
+ # Also mount package directories that need to be served
99
+ files_dir = _PKG_PATH / "files"
100
+ if files_dir.exists():
101
+ app.mount("/files", StaticFiles(directory=str(files_dir)), name="files")
102
+
103
+ etc_dir = _PKG_PATH / "etc"
104
+ if etc_dir.exists():
105
+ app.mount("/etc", StaticFiles(directory=str(etc_dir)), name="etc")
106
+
107
+ # Setup Jinja2 templates
108
+ templates = Jinja2Templates(directory=str(_TEMPLATES_PATH))
109
+ app.state.templates = templates
110
+
111
+ # Register routers
112
+ from zebra_day.web.routers import ui, api
113
+
114
+ app.include_router(ui.router)
115
+ app.include_router(api.router, prefix="/api/v1", tags=["api"])
116
+
117
+ @app.on_event("startup")
118
+ async def startup_event():
119
+ """Initialize application state on startup."""
120
+ import zebra_day.print_mgr as zdpm
121
+
122
+ app.state.zp = zdpm.zpl()
123
+ _log.info(
124
+ "zebra_day web server starting at %s:8118",
125
+ app.state.local_ip,
126
+ )
127
+
128
+ @app.get("/healthz")
129
+ async def healthz():
130
+ """Health check endpoint."""
131
+ return {"status": "healthy"}
132
+
133
+ @app.get("/readyz")
134
+ async def readyz():
135
+ """Readiness check endpoint."""
136
+ # Check if printer manager is initialized
137
+ if hasattr(app.state, "zp") and app.state.zp is not None:
138
+ return {"status": "ready"}
139
+ return {"status": "not_ready"}, 503
140
+
141
+ return app
142
+
143
+
144
+ def get_default_cert_paths() -> tuple[Optional[Path], Optional[Path]]:
145
+ """
146
+ Get default certificate paths from XDG config directory.
147
+
148
+ Returns:
149
+ Tuple of (cert_path, key_path) or (None, None) if not found.
150
+ """
151
+ config_dir = xdg.get_config_dir()
152
+ cert_dir = config_dir / "certs"
153
+ cert_file = cert_dir / "server.crt"
154
+ key_file = cert_dir / "server.key"
155
+
156
+ if cert_file.exists() and key_file.exists():
157
+ return cert_file, key_file
158
+ return None, None
159
+
160
+
161
+ def run_server(
162
+ host: str = "0.0.0.0",
163
+ port: int = 8118,
164
+ reload: bool = False,
165
+ auth: Literal["none", "cognito"] = "none",
166
+ ssl_certfile: Optional[str] = None,
167
+ ssl_keyfile: Optional[str] = None,
168
+ ):
169
+ """
170
+ Run the FastAPI server using uvicorn.
171
+
172
+ Args:
173
+ host: Host to bind to
174
+ port: Port to listen on
175
+ reload: Enable auto-reload for development
176
+ auth: Authentication mode - "none" (public) or "cognito" (AWS Cognito)
177
+ ssl_certfile: Path to SSL certificate file (PEM format)
178
+ ssl_keyfile: Path to SSL private key file (PEM format)
179
+
180
+ If ssl_certfile and ssl_keyfile are not provided, the server will:
181
+ 1. Check SSL_CERT_PATH and SSL_KEY_PATH environment variables
182
+ 2. Check for certificates in ~/.config/zebra_day/certs/
183
+ 3. Fall back to HTTP with a warning if no certificates are found
184
+ """
185
+ import uvicorn
186
+
187
+ # Store auth mode in environment for factory function
188
+ os.environ["ZEBRA_DAY_AUTH_MODE"] = auth
189
+
190
+ # Resolve SSL certificate paths
191
+ cert_path = ssl_certfile
192
+ key_path = ssl_keyfile
193
+
194
+ # Check environment variables if not provided
195
+ if not cert_path:
196
+ cert_path = os.environ.get("SSL_CERT_PATH")
197
+ if not key_path:
198
+ key_path = os.environ.get("SSL_KEY_PATH")
199
+
200
+ # Check default XDG paths if still not found
201
+ if not cert_path or not key_path:
202
+ default_cert, default_key = get_default_cert_paths()
203
+ if default_cert and default_key:
204
+ cert_path = str(default_cert)
205
+ key_path = str(default_key)
206
+
207
+ # Validate certificate files exist
208
+ use_ssl = False
209
+ if cert_path and key_path:
210
+ cert_exists = Path(cert_path).exists()
211
+ key_exists = Path(key_path).exists()
212
+ if cert_exists and key_exists:
213
+ use_ssl = True
214
+ _log.info("HTTPS enabled with certificates:")
215
+ _log.info(" Certificate: %s", cert_path)
216
+ _log.info(" Private key: %s", key_path)
217
+ else:
218
+ if not cert_exists:
219
+ _log.warning("SSL certificate not found: %s", cert_path)
220
+ if not key_exists:
221
+ _log.warning("SSL private key not found: %s", key_path)
222
+ _log.warning("Falling back to HTTP (insecure)")
223
+ else:
224
+ _log.warning(
225
+ "No SSL certificates configured. Running in HTTP mode (insecure). "
226
+ "For HTTPS, run: mkcert -install && mkcert -cert-file ~/.config/zebra_day/certs/server.crt "
227
+ "-key-file ~/.config/zebra_day/certs/server.key localhost 127.0.0.1 ::1"
228
+ )
229
+
230
+ # Build uvicorn config
231
+ uvicorn_kwargs = {
232
+ "host": host,
233
+ "port": port,
234
+ "reload": reload,
235
+ "factory": True,
236
+ }
237
+
238
+ if use_ssl:
239
+ uvicorn_kwargs["ssl_certfile"] = cert_path
240
+ uvicorn_kwargs["ssl_keyfile"] = key_path
241
+ protocol = "https"
242
+ else:
243
+ protocol = "http"
244
+
245
+ _log.info("Starting server at %s://%s:%d", protocol, host, port)
246
+
247
+ uvicorn.run("zebra_day.web.app:create_app", **uvicorn_kwargs)
248
+
zebra_day/web/auth.py ADDED
@@ -0,0 +1,172 @@
1
+ """Authentication integration for zebra_day web server.
2
+
3
+ Provides optional Cognito authentication support via the daylily-cognito library.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional
10
+
11
+ from fastapi import Depends, HTTPException, Request, status
12
+ from starlette.middleware.base import BaseHTTPMiddleware
13
+ from starlette.responses import Response
14
+
15
+ from zebra_day.logging_config import get_logger
16
+
17
+ if TYPE_CHECKING:
18
+ from fastapi import FastAPI
19
+
20
+ _log = get_logger(__name__)
21
+
22
+ # Endpoints that should never require authentication
23
+ PUBLIC_PATHS: List[str] = [
24
+ "/healthz",
25
+ "/readyz",
26
+ "/docs",
27
+ "/openapi.json",
28
+ "/redoc",
29
+ ]
30
+
31
+ # Try to import daylily-cognito components
32
+ _COGNITO_AVAILABLE = False
33
+ _COGNITO_IMPORT_ERROR: Optional[str] = None
34
+
35
+ try:
36
+ from daylily_cognito import CognitoAuth, CognitoConfig, create_auth_dependency
37
+
38
+ _COGNITO_AVAILABLE = True
39
+ except ImportError as e:
40
+ _COGNITO_IMPORT_ERROR = str(e)
41
+ CognitoAuth = None # type: ignore[misc, assignment]
42
+ CognitoConfig = None # type: ignore[misc, assignment]
43
+ create_auth_dependency = None # type: ignore[misc, assignment]
44
+
45
+
46
+ def is_cognito_available() -> bool:
47
+ """Check if daylily-cognito library is installed."""
48
+ return _COGNITO_AVAILABLE
49
+
50
+
51
+ def get_cognito_import_error() -> Optional[str]:
52
+ """Get the import error message if daylily-cognito is not available."""
53
+ return _COGNITO_IMPORT_ERROR
54
+
55
+
56
+ class CognitoAuthMiddleware(BaseHTTPMiddleware):
57
+ """Middleware that enforces Cognito authentication on protected endpoints.
58
+
59
+ Exempts health check endpoints and other public paths.
60
+ """
61
+
62
+ def __init__(self, app: "FastAPI", cognito_auth: Any) -> None:
63
+ super().__init__(app)
64
+ self.cognito_auth = cognito_auth
65
+ self.get_current_user = create_auth_dependency(cognito_auth, optional=False)
66
+
67
+ async def dispatch(self, request: Request, call_next: Callable) -> Response:
68
+ """Check authentication for protected endpoints."""
69
+ path = request.url.path
70
+
71
+ # Allow public endpoints without authentication
72
+ if any(path.startswith(public) for public in PUBLIC_PATHS):
73
+ return await call_next(request)
74
+
75
+ # Allow static files without authentication
76
+ if path.startswith("/static") or path.startswith("/files") or path.startswith("/etc"):
77
+ return await call_next(request)
78
+
79
+ # Check for Authorization header
80
+ auth_header = request.headers.get("Authorization")
81
+ if not auth_header:
82
+ return Response(
83
+ content='{"detail":"Authentication required"}',
84
+ status_code=status.HTTP_401_UNAUTHORIZED,
85
+ media_type="application/json",
86
+ headers={"WWW-Authenticate": "Bearer"},
87
+ )
88
+
89
+ # Validate Bearer token
90
+ if not auth_header.startswith("Bearer "):
91
+ return Response(
92
+ content='{"detail":"Invalid authorization header format"}',
93
+ status_code=status.HTTP_401_UNAUTHORIZED,
94
+ media_type="application/json",
95
+ headers={"WWW-Authenticate": "Bearer"},
96
+ )
97
+
98
+ token = auth_header[7:] # Remove "Bearer " prefix
99
+ try:
100
+ # Verify token and attach user to request state
101
+ user_claims = self.cognito_auth.verify_token(token)
102
+ request.state.user = user_claims
103
+ except HTTPException:
104
+ return Response(
105
+ content='{"detail":"Invalid or expired token"}',
106
+ status_code=status.HTTP_401_UNAUTHORIZED,
107
+ media_type="application/json",
108
+ headers={"WWW-Authenticate": "Bearer"},
109
+ )
110
+ except Exception as e:
111
+ _log.error("Authentication error: %s", str(e))
112
+ return Response(
113
+ content='{"detail":"Authentication failed"}',
114
+ status_code=status.HTTP_401_UNAUTHORIZED,
115
+ media_type="application/json",
116
+ headers={"WWW-Authenticate": "Bearer"},
117
+ )
118
+
119
+ return await call_next(request)
120
+
121
+
122
+ def setup_cognito_auth(app: "FastAPI") -> Any:
123
+ """Configure Cognito authentication for the FastAPI app.
124
+
125
+ Reads configuration from environment variables:
126
+ - COGNITO_USER_POOL_ID: Cognito User Pool ID (required)
127
+ - COGNITO_APP_CLIENT_ID or COGNITO_CLIENT_ID: App Client ID (required)
128
+ - COGNITO_REGION or AWS_REGION: AWS region (defaults to us-west-2)
129
+ - AWS_PROFILE: Optional AWS profile name
130
+
131
+ Returns:
132
+ CognitoAuth instance
133
+
134
+ Raises:
135
+ ValueError: If required environment variables are missing
136
+ ImportError: If daylily-cognito is not installed
137
+ """
138
+ if not _COGNITO_AVAILABLE:
139
+ raise ImportError(
140
+ f"daylily-cognito library is required for Cognito authentication. "
141
+ f"Install with: pip install -e '.[auth]'\n"
142
+ f"Import error: {_COGNITO_IMPORT_ERROR}"
143
+ )
144
+
145
+ # Load config from environment
146
+ try:
147
+ config = CognitoConfig.from_legacy_env()
148
+ except ValueError as e:
149
+ raise ValueError(
150
+ f"Missing required Cognito configuration. {e}\n"
151
+ "Set the following environment variables:\n"
152
+ " COGNITO_USER_POOL_ID=your-pool-id\n"
153
+ " COGNITO_APP_CLIENT_ID=your-client-id\n"
154
+ " COGNITO_REGION=us-west-2 (optional, defaults to us-west-2)"
155
+ ) from e
156
+
157
+ # Create CognitoAuth instance
158
+ cognito_auth = CognitoAuth(
159
+ region=config.region,
160
+ user_pool_id=config.user_pool_id,
161
+ app_client_id=config.app_client_id,
162
+ profile=config.aws_profile,
163
+ )
164
+
165
+ _log.info(
166
+ "Cognito authentication enabled (region=%s, pool=%s)",
167
+ config.region,
168
+ config.user_pool_id,
169
+ )
170
+
171
+ return cognito_auth
172
+
@@ -0,0 +1,159 @@
1
+ """
2
+ Middleware for the zebra_day FastAPI application.
3
+
4
+ Provides request logging and rate limiting functionality.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import asyncio
9
+ import time
10
+ from collections import defaultdict
11
+ from typing import Callable
12
+
13
+ from fastapi import Request, Response
14
+ from starlette.middleware.base import BaseHTTPMiddleware
15
+
16
+ from zebra_day.logging_config import get_logger
17
+
18
+ _log = get_logger(__name__)
19
+
20
+
21
+ class RequestLoggingMiddleware(BaseHTTPMiddleware):
22
+ """
23
+ Middleware for structured request logging.
24
+
25
+ Logs client IP, request path, method, timing, and response status.
26
+ """
27
+
28
+ async def dispatch(self, request: Request, call_next: Callable) -> Response:
29
+ """Process request and log structured data."""
30
+ start_time = time.perf_counter()
31
+
32
+ # Extract client info
33
+ client_ip = request.client.host if request.client else "unknown"
34
+ method = request.method
35
+ path = request.url.path
36
+ query = str(request.query_params) if request.query_params else ""
37
+
38
+ # Extract relevant parameters for print operations
39
+ lab = request.query_params.get("lab", "")
40
+ printer = request.query_params.get("printer", "")
41
+ template = request.query_params.get("label_zpl_style", "")
42
+
43
+ try:
44
+ response = await call_next(request)
45
+ status_code = response.status_code
46
+ outcome = "success" if status_code < 400 else "error"
47
+ except Exception as exc:
48
+ status_code = 500
49
+ outcome = "exception"
50
+ _log.exception(
51
+ "Request failed",
52
+ extra={
53
+ "client_ip": client_ip,
54
+ "method": method,
55
+ "path": path,
56
+ "error": str(exc),
57
+ },
58
+ )
59
+ raise
60
+
61
+ elapsed_ms = (time.perf_counter() - start_time) * 1000
62
+
63
+ # Build log context
64
+ log_context = {
65
+ "client_ip": client_ip,
66
+ "method": method,
67
+ "path": path,
68
+ "status_code": status_code,
69
+ "elapsed_ms": round(elapsed_ms, 2),
70
+ "outcome": outcome,
71
+ }
72
+
73
+ # Add print-specific context if relevant
74
+ if lab:
75
+ log_context["lab"] = lab
76
+ if printer:
77
+ log_context["printer"] = printer
78
+ if template:
79
+ log_context["template"] = template
80
+
81
+ # Log at appropriate level
82
+ if status_code >= 500:
83
+ _log.error("Request completed", extra=log_context)
84
+ elif status_code >= 400:
85
+ _log.warning("Request completed", extra=log_context)
86
+ else:
87
+ _log.info("Request completed", extra=log_context)
88
+
89
+ return response
90
+
91
+
92
+ class PrintRateLimiter:
93
+ """
94
+ Simple rate limiter for print endpoints.
95
+
96
+ Uses a sliding window approach with configurable limits.
97
+ """
98
+
99
+ def __init__(
100
+ self,
101
+ max_requests: int = 10,
102
+ window_seconds: float = 60.0,
103
+ max_concurrent: int = 3,
104
+ ):
105
+ """
106
+ Initialize rate limiter.
107
+
108
+ Args:
109
+ max_requests: Maximum requests per window per client IP
110
+ window_seconds: Time window in seconds
111
+ max_concurrent: Maximum concurrent print operations
112
+ """
113
+ self.max_requests = max_requests
114
+ self.window_seconds = window_seconds
115
+ self.max_concurrent = max_concurrent
116
+
117
+ self._request_times: dict[str, list[float]] = defaultdict(list)
118
+ self._semaphore = asyncio.Semaphore(max_concurrent)
119
+ self._lock = asyncio.Lock()
120
+
121
+ async def acquire(self, client_ip: str) -> tuple[bool, str]:
122
+ """
123
+ Try to acquire a print slot.
124
+
125
+ Returns:
126
+ Tuple of (allowed, reason)
127
+ """
128
+ now = time.time()
129
+
130
+ async with self._lock:
131
+ # Clean old entries
132
+ cutoff = now - self.window_seconds
133
+ self._request_times[client_ip] = [
134
+ t for t in self._request_times[client_ip] if t > cutoff
135
+ ]
136
+
137
+ # Check rate limit
138
+ if len(self._request_times[client_ip]) >= self.max_requests:
139
+ return False, f"Rate limit exceeded: {self.max_requests} requests per {self.window_seconds}s"
140
+
141
+ # Try to acquire semaphore (non-blocking check)
142
+ if self._semaphore.locked() and self._semaphore._value == 0:
143
+ return False, f"Too many concurrent print operations (max {self.max_concurrent})"
144
+
145
+ # Record this request
146
+ self._request_times[client_ip].append(now)
147
+
148
+ # Acquire semaphore for actual operation
149
+ await self._semaphore.acquire()
150
+ return True, ""
151
+
152
+ def release(self) -> None:
153
+ """Release a print slot after operation completes."""
154
+ self._semaphore.release()
155
+
156
+
157
+ # Global rate limiter instance
158
+ print_rate_limiter = PrintRateLimiter()
159
+
@@ -0,0 +1,2 @@
1
+ """FastAPI routers for zebra_day web application."""
2
+