zebra-day 2.0.0__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.
- zebra_day/__init__.py +7 -2
- zebra_day/_version.py +1 -0
- zebra_day/cli/__init__.py +80 -30
- zebra_day/cli/cognito.py +15 -9
- zebra_day/cli/gui.py +21 -16
- zebra_day/cli/printer.py +34 -27
- zebra_day/cli/template.py +19 -15
- zebra_day/cmd_mgr.py +3 -6
- zebra_day/docs/gx420d-gx430d-ug-en.pdf +0 -0
- zebra_day/docs/hardware_config_guide.md +149 -0
- zebra_day/docs/programatic_guide.md +181 -0
- zebra_day/docs/qln420_zebra_manual.pdf +0 -0
- zebra_day/docs/uid_screed_light.md +38 -0
- zebra_day/docs/zd620-zd420-ug-en.pdf +0 -0
- zebra_day/docs/zebra_day_ui_guide.md +194 -0
- zebra_day/etc/printer_config.json +7 -1
- zebra_day/etc/printer_config.template.json +3 -17
- zebra_day/etc/tmp_printers139.json +10 -0
- zebra_day/etc/tmp_printers147.json +10 -0
- zebra_day/etc/tmp_printers34.json +10 -0
- zebra_day/etc/tmp_printers389.json +10 -0
- zebra_day/etc/tmp_printers398.json +10 -0
- zebra_day/etc/tmp_printers437.json +10 -0
- zebra_day/etc/tmp_printers439.json +10 -0
- zebra_day/etc/tmp_printers440.json +10 -0
- zebra_day/etc/tmp_printers508.json +10 -0
- zebra_day/etc/tmp_printers543.json +10 -0
- zebra_day/etc/tmp_printers835.json +10 -0
- zebra_day/etc/tmp_printers842.json +10 -0
- zebra_day/etc/tmp_printers931.json +10 -0
- zebra_day/etc/tmp_printers969.json +10 -0
- zebra_day/exceptions.py +1 -1
- zebra_day/files/corners_smallTube_preview.png +0 -0
- zebra_day/files/test_png_2897.png +0 -0
- zebra_day/files/test_png_31690.png +0 -0
- zebra_day/files/test_png_33804.png +0 -0
- zebra_day/files/test_png_34737.png +0 -0
- zebra_day/files/test_png_4161.png +0 -0
- zebra_day/files/test_png_44748.png +0 -0
- zebra_day/files/test_png_4635.png +0 -0
- zebra_day/files/test_png_56349.png +0 -0
- zebra_day/files/test_png_5936.png +0 -0
- zebra_day/files/test_png_64110.png +0 -0
- zebra_day/files/test_png_64891.png +0 -0
- zebra_day/files/test_png_69002.png +0 -0
- zebra_day/files/test_png_70065.png +0 -0
- zebra_day/files/test_png_72366.png +0 -0
- zebra_day/files/test_png_77793.png +0 -0
- zebra_day/files/test_png_9572.png +0 -0
- zebra_day/imgs/.hold +0 -0
- zebra_day/imgs/bar_ltpurp.png +0 -0
- zebra_day/imgs/bar_purp.png +0 -0
- zebra_day/imgs/bar_purp3.png +0 -0
- zebra_day/imgs/bar_red.png +0 -0
- zebra_day/imgs/legacy/UBC_gantt_chart.png +0 -0
- zebra_day/imgs/legacy/gx420d_network_config.png +0 -0
- zebra_day/imgs/legacy/gx420d_printer_config.png +0 -0
- zebra_day/imgs/legacy/ngrok.png +0 -0
- zebra_day/imgs/legacy/printer_details.png +0 -0
- zebra_day/imgs/legacy/quick_start_test_label.png +0 -0
- zebra_day/imgs/legacy/quick_start_test_label2.png +0 -0
- zebra_day/imgs/legacy/zd620_network_config.png +0 -0
- zebra_day/imgs/legacy/zd620_printer_config.png +0 -0
- zebra_day/imgs/legacy/zday_quick_gui.png +0 -0
- zebra_day/imgs/legacy/zebra_day_alt_css_dog.png +0 -0
- zebra_day/imgs/legacy/zebra_day_alt_css_flower.png +0 -0
- zebra_day/imgs/legacy/zebra_day_alt_css_main.png +0 -0
- zebra_day/imgs/legacy/zebra_day_available_zpl_templates.png +0 -0
- zebra_day/imgs/legacy/zebra_day_bkup_pconfig.png +0 -0
- zebra_day/imgs/legacy/zebra_day_home.png +0 -0
- zebra_day/imgs/legacy/zebra_day_manual_print.png +0 -0
- zebra_day/imgs/legacy/zebra_day_printer_fleet_json.png +0 -0
- zebra_day/imgs/legacy/zebra_day_quick_ex.png +0 -0
- zebra_day/imgs/legacy/zebra_day_zpl_template_IRLa.png +0 -0
- zebra_day/imgs/legacy/zebra_day_zpl_template_IRLb.png +0 -0
- zebra_day/imgs/ui_api_docs.png +0 -0
- zebra_day/imgs/ui_config.png +0 -0
- zebra_day/imgs/ui_dashboard.png +0 -0
- zebra_day/imgs/ui_print_request.png +0 -0
- zebra_day/imgs/ui_printers.png +0 -0
- zebra_day/imgs/ui_templates.png +0 -0
- zebra_day/logging_config.py +4 -9
- zebra_day/mkcert.py +157 -0
- zebra_day/paths.py +1 -2
- zebra_day/print_mgr.py +165 -145
- zebra_day/templates/modern/config.html +7 -0
- zebra_day/templates/modern/print_request.html +61 -3
- zebra_day/web/__init__.py +1 -1
- zebra_day/web/app.py +21 -16
- zebra_day/web/auth.py +17 -15
- zebra_day/web/middleware.py +8 -5
- zebra_day/web/routers/__init__.py +0 -1
- zebra_day/web/routers/api.py +192 -43
- zebra_day/web/routers/ui.py +31 -33
- zebra_day/zpl_renderer.py +45 -34
- {zebra_day-2.0.0.dist-info → zebra_day-2.1.4.dist-info}/METADATA +76 -67
- {zebra_day-2.0.0.dist-info → zebra_day-2.1.4.dist-info}/RECORD +101 -29
- {zebra_day-2.0.0.dist-info → zebra_day-2.1.4.dist-info}/WHEEL +0 -0
- {zebra_day-2.0.0.dist-info → zebra_day-2.1.4.dist-info}/entry_points.txt +0 -0
- {zebra_day-2.0.0.dist-info → zebra_day-2.1.4.dist-info}/licenses/LICENSE +0 -0
- {zebra_day-2.0.0.dist-info → zebra_day-2.1.4.dist-info}/top_level.txt +0 -0
|
@@ -96,9 +96,14 @@
|
|
|
96
96
|
<a href="/" class="btn btn-outline">
|
|
97
97
|
<i class="fas fa-arrow-left"></i> Cancel
|
|
98
98
|
</a>
|
|
99
|
-
<
|
|
100
|
-
<
|
|
101
|
-
|
|
99
|
+
<div class="d-flex" style="gap: var(--spacing-md);">
|
|
100
|
+
<button type="button" class="btn btn-secondary btn-lg" onclick="downloadPNG()">
|
|
101
|
+
<i class="fas fa-download"></i> Download PNG
|
|
102
|
+
</button>
|
|
103
|
+
<button type="submit" class="btn btn-primary btn-lg" onclick="showLoading('Sending print request...')">
|
|
104
|
+
<i class="fas fa-print"></i> Print Label
|
|
105
|
+
</button>
|
|
106
|
+
</div>
|
|
102
107
|
</div>
|
|
103
108
|
</div>
|
|
104
109
|
</form>
|
|
@@ -140,6 +145,59 @@
|
|
|
140
145
|
updatePrinters();
|
|
141
146
|
}
|
|
142
147
|
});
|
|
148
|
+
|
|
149
|
+
async function downloadPNG() {
|
|
150
|
+
var form = document.getElementById('print-form');
|
|
151
|
+
var template = form.querySelector('select[name="label_zpl_style"]').value;
|
|
152
|
+
|
|
153
|
+
if (!template) {
|
|
154
|
+
showToast('error', 'Error', 'Please select a label template');
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
showLoading('Rendering PNG...');
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
var response = await fetch('/api/v1/render/png', {
|
|
162
|
+
method: 'POST',
|
|
163
|
+
headers: {'Content-Type': 'application/json'},
|
|
164
|
+
body: JSON.stringify({
|
|
165
|
+
template: template,
|
|
166
|
+
uid_barcode: form.querySelector('input[name="uid_barcode"]').value || '',
|
|
167
|
+
alt_a: form.querySelector('input[name="alt_a"]').value || '',
|
|
168
|
+
alt_b: form.querySelector('input[name="alt_b"]').value || '',
|
|
169
|
+
alt_c: form.querySelector('input[name="alt_c"]').value || '',
|
|
170
|
+
alt_d: form.querySelector('input[name="alt_d"]').value || '',
|
|
171
|
+
alt_e: form.querySelector('input[name="alt_e"]').value || '',
|
|
172
|
+
alt_f: form.querySelector('input[name="alt_f"]').value || ''
|
|
173
|
+
})
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
hideLoading();
|
|
177
|
+
|
|
178
|
+
if (!response.ok) {
|
|
179
|
+
var error = await response.json();
|
|
180
|
+
showToast('error', 'Render Failed', error.detail || 'Unknown error');
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Download the PNG file
|
|
185
|
+
var blob = await response.blob();
|
|
186
|
+
var url = window.URL.createObjectURL(blob);
|
|
187
|
+
var a = document.createElement('a');
|
|
188
|
+
a.href = url;
|
|
189
|
+
a.download = template + '_label.png';
|
|
190
|
+
document.body.appendChild(a);
|
|
191
|
+
a.click();
|
|
192
|
+
window.URL.revokeObjectURL(url);
|
|
193
|
+
a.remove();
|
|
194
|
+
|
|
195
|
+
showToast('success', 'Success', 'PNG downloaded');
|
|
196
|
+
} catch (e) {
|
|
197
|
+
hideLoading();
|
|
198
|
+
showToast('error', 'Error', 'Failed to render PNG: ' + e.message);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
143
201
|
</script>
|
|
144
202
|
{% endblock %}
|
|
145
203
|
|
zebra_day/web/__init__.py
CHANGED
zebra_day/web/app.py
CHANGED
|
@@ -3,20 +3,21 @@ FastAPI application factory for zebra_day.
|
|
|
3
3
|
|
|
4
4
|
This module provides the main FastAPI application for the zebra_day web interface.
|
|
5
5
|
"""
|
|
6
|
+
|
|
6
7
|
from __future__ import annotations
|
|
7
8
|
|
|
8
9
|
import os
|
|
9
10
|
import subprocess
|
|
11
|
+
from importlib.resources import files
|
|
10
12
|
from pathlib import Path
|
|
11
|
-
from typing import Literal
|
|
13
|
+
from typing import Literal
|
|
12
14
|
|
|
13
|
-
from fastapi import FastAPI
|
|
15
|
+
from fastapi import FastAPI
|
|
14
16
|
from fastapi.staticfiles import StaticFiles
|
|
15
17
|
from fastapi.templating import Jinja2Templates
|
|
16
|
-
from importlib.resources import files
|
|
17
18
|
|
|
18
|
-
from zebra_day.logging_config import get_logger
|
|
19
19
|
from zebra_day import paths as xdg
|
|
20
|
+
from zebra_day.logging_config import get_logger
|
|
20
21
|
from zebra_day.web.middleware import RequestLoggingMiddleware, print_rate_limiter
|
|
21
22
|
|
|
22
23
|
_log = get_logger(__name__)
|
|
@@ -39,7 +40,7 @@ def create_app(
|
|
|
39
40
|
*,
|
|
40
41
|
debug: bool = False,
|
|
41
42
|
css_theme: str = "lsmc.css",
|
|
42
|
-
auth:
|
|
43
|
+
auth: Literal["none", "cognito"] | None = None,
|
|
43
44
|
) -> FastAPI:
|
|
44
45
|
"""
|
|
45
46
|
Create and configure the FastAPI application.
|
|
@@ -76,7 +77,7 @@ def create_app(
|
|
|
76
77
|
from zebra_day.web.auth import CognitoAuthMiddleware, setup_cognito_auth
|
|
77
78
|
|
|
78
79
|
cognito_auth = setup_cognito_auth(app)
|
|
79
|
-
app.add_middleware(CognitoAuthMiddleware, cognito_auth=cognito_auth)
|
|
80
|
+
app.add_middleware(CognitoAuthMiddleware, cognito_auth=cognito_auth) # type: ignore[arg-type]
|
|
80
81
|
app.state.cognito_auth = cognito_auth
|
|
81
82
|
app.state.auth_mode = "cognito"
|
|
82
83
|
_log.info("Cognito authentication middleware enabled")
|
|
@@ -96,9 +97,14 @@ def create_app(
|
|
|
96
97
|
app.mount("/static", StaticFiles(directory=str(_STATIC_PATH)), name="static")
|
|
97
98
|
|
|
98
99
|
# Also mount package directories that need to be served
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
# Package files directory (for templates, previews generated in-package)
|
|
101
|
+
pkg_files_dir = _PKG_PATH / "files"
|
|
102
|
+
pkg_files_dir.mkdir(parents=True, exist_ok=True)
|
|
103
|
+
app.mount("/files", StaticFiles(directory=str(pkg_files_dir)), name="files")
|
|
104
|
+
|
|
105
|
+
# XDG generated files directory (for PNG downloads from dl_png printer)
|
|
106
|
+
xdg_generated_dir = xdg.get_generated_files_dir()
|
|
107
|
+
app.mount("/generated", StaticFiles(directory=str(xdg_generated_dir)), name="generated")
|
|
102
108
|
|
|
103
109
|
etc_dir = _PKG_PATH / "etc"
|
|
104
110
|
if etc_dir.exists():
|
|
@@ -109,7 +115,7 @@ def create_app(
|
|
|
109
115
|
app.state.templates = templates
|
|
110
116
|
|
|
111
117
|
# Register routers
|
|
112
|
-
from zebra_day.web.routers import
|
|
118
|
+
from zebra_day.web.routers import api, ui
|
|
113
119
|
|
|
114
120
|
app.include_router(ui.router)
|
|
115
121
|
app.include_router(api.router, prefix="/api/v1", tags=["api"])
|
|
@@ -141,7 +147,7 @@ def create_app(
|
|
|
141
147
|
return app
|
|
142
148
|
|
|
143
149
|
|
|
144
|
-
def get_default_cert_paths() -> tuple[
|
|
150
|
+
def get_default_cert_paths() -> tuple[Path | None, Path | None]:
|
|
145
151
|
"""
|
|
146
152
|
Get default certificate paths from XDG config directory.
|
|
147
153
|
|
|
@@ -163,8 +169,8 @@ def run_server(
|
|
|
163
169
|
port: int = 8118,
|
|
164
170
|
reload: bool = False,
|
|
165
171
|
auth: Literal["none", "cognito"] = "none",
|
|
166
|
-
ssl_certfile:
|
|
167
|
-
ssl_keyfile:
|
|
172
|
+
ssl_certfile: str | None = None,
|
|
173
|
+
ssl_keyfile: str | None = None,
|
|
168
174
|
):
|
|
169
175
|
"""
|
|
170
176
|
Run the FastAPI server using uvicorn.
|
|
@@ -228,7 +234,7 @@ def run_server(
|
|
|
228
234
|
)
|
|
229
235
|
|
|
230
236
|
# Build uvicorn config
|
|
231
|
-
uvicorn_kwargs = {
|
|
237
|
+
uvicorn_kwargs: dict[str, str | int | bool | None] = {
|
|
232
238
|
"host": host,
|
|
233
239
|
"port": port,
|
|
234
240
|
"reload": reload,
|
|
@@ -244,5 +250,4 @@ def run_server(
|
|
|
244
250
|
|
|
245
251
|
_log.info("Starting server at %s://%s:%d", protocol, host, port)
|
|
246
252
|
|
|
247
|
-
uvicorn.run("zebra_day.web.app:create_app", **uvicorn_kwargs)
|
|
248
|
-
|
|
253
|
+
uvicorn.run("zebra_day.web.app:create_app", **uvicorn_kwargs) # type: ignore[arg-type]
|
zebra_day/web/auth.py
CHANGED
|
@@ -5,10 +5,10 @@ Provides optional Cognito authentication support via the daylily-cognito library
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
-
import
|
|
9
|
-
from typing import TYPE_CHECKING, Any
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
10
|
|
|
11
|
-
from fastapi import
|
|
11
|
+
from fastapi import HTTPException, Request, status
|
|
12
12
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
13
13
|
from starlette.responses import Response
|
|
14
14
|
|
|
@@ -20,7 +20,7 @@ if TYPE_CHECKING:
|
|
|
20
20
|
_log = get_logger(__name__)
|
|
21
21
|
|
|
22
22
|
# Endpoints that should never require authentication
|
|
23
|
-
PUBLIC_PATHS:
|
|
23
|
+
PUBLIC_PATHS: list[str] = [
|
|
24
24
|
"/healthz",
|
|
25
25
|
"/readyz",
|
|
26
26
|
"/docs",
|
|
@@ -30,7 +30,7 @@ PUBLIC_PATHS: List[str] = [
|
|
|
30
30
|
|
|
31
31
|
# Try to import daylily-cognito components
|
|
32
32
|
_COGNITO_AVAILABLE = False
|
|
33
|
-
_COGNITO_IMPORT_ERROR:
|
|
33
|
+
_COGNITO_IMPORT_ERROR: str | None = None
|
|
34
34
|
|
|
35
35
|
try:
|
|
36
36
|
from daylily_cognito import CognitoAuth, CognitoConfig, create_auth_dependency
|
|
@@ -38,9 +38,9 @@ try:
|
|
|
38
38
|
_COGNITO_AVAILABLE = True
|
|
39
39
|
except ImportError as e:
|
|
40
40
|
_COGNITO_IMPORT_ERROR = str(e)
|
|
41
|
-
CognitoAuth = None
|
|
42
|
-
CognitoConfig = None
|
|
43
|
-
create_auth_dependency = None
|
|
41
|
+
CognitoAuth = None
|
|
42
|
+
CognitoConfig = None
|
|
43
|
+
create_auth_dependency = None
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
def is_cognito_available() -> bool:
|
|
@@ -48,7 +48,7 @@ def is_cognito_available() -> bool:
|
|
|
48
48
|
return _COGNITO_AVAILABLE
|
|
49
49
|
|
|
50
50
|
|
|
51
|
-
def get_cognito_import_error() ->
|
|
51
|
+
def get_cognito_import_error() -> str | None:
|
|
52
52
|
"""Get the import error message if daylily-cognito is not available."""
|
|
53
53
|
return _COGNITO_IMPORT_ERROR
|
|
54
54
|
|
|
@@ -59,7 +59,7 @@ class CognitoAuthMiddleware(BaseHTTPMiddleware):
|
|
|
59
59
|
Exempts health check endpoints and other public paths.
|
|
60
60
|
"""
|
|
61
61
|
|
|
62
|
-
def __init__(self, app:
|
|
62
|
+
def __init__(self, app: FastAPI, cognito_auth: Any) -> None:
|
|
63
63
|
super().__init__(app)
|
|
64
64
|
self.cognito_auth = cognito_auth
|
|
65
65
|
self.get_current_user = create_auth_dependency(cognito_auth, optional=False)
|
|
@@ -70,11 +70,13 @@ class CognitoAuthMiddleware(BaseHTTPMiddleware):
|
|
|
70
70
|
|
|
71
71
|
# Allow public endpoints without authentication
|
|
72
72
|
if any(path.startswith(public) for public in PUBLIC_PATHS):
|
|
73
|
-
|
|
73
|
+
response = await call_next(request)
|
|
74
|
+
return response # type: ignore[no-any-return]
|
|
74
75
|
|
|
75
76
|
# Allow static files without authentication
|
|
76
77
|
if path.startswith("/static") or path.startswith("/files") or path.startswith("/etc"):
|
|
77
|
-
|
|
78
|
+
response = await call_next(request)
|
|
79
|
+
return response # type: ignore[no-any-return]
|
|
78
80
|
|
|
79
81
|
# Check for Authorization header
|
|
80
82
|
auth_header = request.headers.get("Authorization")
|
|
@@ -116,10 +118,11 @@ class CognitoAuthMiddleware(BaseHTTPMiddleware):
|
|
|
116
118
|
headers={"WWW-Authenticate": "Bearer"},
|
|
117
119
|
)
|
|
118
120
|
|
|
119
|
-
|
|
121
|
+
response = await call_next(request)
|
|
122
|
+
return response # type: ignore[no-any-return]
|
|
120
123
|
|
|
121
124
|
|
|
122
|
-
def setup_cognito_auth(app:
|
|
125
|
+
def setup_cognito_auth(app: FastAPI) -> Any:
|
|
123
126
|
"""Configure Cognito authentication for the FastAPI app.
|
|
124
127
|
|
|
125
128
|
Reads configuration from environment variables:
|
|
@@ -169,4 +172,3 @@ def setup_cognito_auth(app: "FastAPI") -> Any:
|
|
|
169
172
|
)
|
|
170
173
|
|
|
171
174
|
return cognito_auth
|
|
172
|
-
|
zebra_day/web/middleware.py
CHANGED
|
@@ -3,12 +3,13 @@ Middleware for the zebra_day FastAPI application.
|
|
|
3
3
|
|
|
4
4
|
Provides request logging and rate limiting functionality.
|
|
5
5
|
"""
|
|
6
|
+
|
|
6
7
|
from __future__ import annotations
|
|
7
8
|
|
|
8
9
|
import asyncio
|
|
9
10
|
import time
|
|
10
11
|
from collections import defaultdict
|
|
11
|
-
from
|
|
12
|
+
from collections.abc import Callable
|
|
12
13
|
|
|
13
14
|
from fastapi import Request, Response
|
|
14
15
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
@@ -33,7 +34,7 @@ class RequestLoggingMiddleware(BaseHTTPMiddleware):
|
|
|
33
34
|
client_ip = request.client.host if request.client else "unknown"
|
|
34
35
|
method = request.method
|
|
35
36
|
path = request.url.path
|
|
36
|
-
|
|
37
|
+
str(request.query_params) if request.query_params else ""
|
|
37
38
|
|
|
38
39
|
# Extract relevant parameters for print operations
|
|
39
40
|
lab = request.query_params.get("lab", "")
|
|
@@ -86,7 +87,7 @@ class RequestLoggingMiddleware(BaseHTTPMiddleware):
|
|
|
86
87
|
else:
|
|
87
88
|
_log.info("Request completed", extra=log_context)
|
|
88
89
|
|
|
89
|
-
return response
|
|
90
|
+
return response # type: ignore[no-any-return]
|
|
90
91
|
|
|
91
92
|
|
|
92
93
|
class PrintRateLimiter:
|
|
@@ -136,7 +137,10 @@ class PrintRateLimiter:
|
|
|
136
137
|
|
|
137
138
|
# Check rate limit
|
|
138
139
|
if len(self._request_times[client_ip]) >= self.max_requests:
|
|
139
|
-
return
|
|
140
|
+
return (
|
|
141
|
+
False,
|
|
142
|
+
f"Rate limit exceeded: {self.max_requests} requests per {self.window_seconds}s",
|
|
143
|
+
)
|
|
140
144
|
|
|
141
145
|
# Try to acquire semaphore (non-blocking check)
|
|
142
146
|
if self._semaphore.locked() and self._semaphore._value == 0:
|
|
@@ -156,4 +160,3 @@ class PrintRateLimiter:
|
|
|
156
160
|
|
|
157
161
|
# Global rate limiter instance
|
|
158
162
|
print_rate_limiter = PrintRateLimiter()
|
|
159
|
-
|