zenml-nightly 0.83.0.dev20250618__py3-none-any.whl → 0.83.0.dev20250621__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.
- zenml/VERSION +1 -1
- zenml/__init__.py +12 -2
- zenml/analytics/context.py +4 -2
- zenml/config/server_config.py +6 -1
- zenml/constants.py +3 -0
- zenml/entrypoints/step_entrypoint_configuration.py +14 -0
- zenml/models/__init__.py +15 -0
- zenml/models/v2/core/api_transaction.py +193 -0
- zenml/models/v2/core/pipeline_build.py +4 -0
- zenml/models/v2/core/pipeline_deployment.py +8 -1
- zenml/models/v2/core/pipeline_run.py +7 -0
- zenml/models/v2/core/step_run.py +6 -0
- zenml/orchestrators/input_utils.py +34 -11
- zenml/utils/json_utils.py +1 -1
- zenml/zen_server/auth.py +53 -31
- zenml/zen_server/cloud_utils.py +19 -7
- zenml/zen_server/middleware.py +424 -0
- zenml/zen_server/rbac/endpoint_utils.py +5 -2
- zenml/zen_server/rbac/utils.py +12 -7
- zenml/zen_server/request_management.py +556 -0
- zenml/zen_server/routers/auth_endpoints.py +1 -0
- zenml/zen_server/routers/model_versions_endpoints.py +3 -3
- zenml/zen_server/routers/models_endpoints.py +3 -3
- zenml/zen_server/routers/pipeline_builds_endpoints.py +2 -2
- zenml/zen_server/routers/pipeline_deployments_endpoints.py +9 -4
- zenml/zen_server/routers/pipelines_endpoints.py +4 -4
- zenml/zen_server/routers/run_templates_endpoints.py +3 -3
- zenml/zen_server/routers/runs_endpoints.py +4 -4
- zenml/zen_server/routers/service_connectors_endpoints.py +6 -6
- zenml/zen_server/routers/steps_endpoints.py +3 -3
- zenml/zen_server/utils.py +230 -63
- zenml/zen_server/zen_server_api.py +34 -399
- zenml/zen_stores/migrations/versions/3d7e39f3ac92_split_up_step_configurations.py +138 -0
- zenml/zen_stores/migrations/versions/857843db1bcf_add_api_transaction_table.py +69 -0
- zenml/zen_stores/rest_zen_store.py +52 -42
- zenml/zen_stores/schemas/__init__.py +4 -0
- zenml/zen_stores/schemas/api_transaction_schemas.py +141 -0
- zenml/zen_stores/schemas/pipeline_deployment_schemas.py +88 -27
- zenml/zen_stores/schemas/pipeline_run_schemas.py +28 -11
- zenml/zen_stores/schemas/step_run_schemas.py +4 -4
- zenml/zen_stores/sql_zen_store.py +277 -42
- zenml/zen_stores/zen_store_interface.py +7 -1
- {zenml_nightly-0.83.0.dev20250618.dist-info → zenml_nightly-0.83.0.dev20250621.dist-info}/METADATA +1 -1
- {zenml_nightly-0.83.0.dev20250618.dist-info → zenml_nightly-0.83.0.dev20250621.dist-info}/RECORD +47 -41
- {zenml_nightly-0.83.0.dev20250618.dist-info → zenml_nightly-0.83.0.dev20250621.dist-info}/LICENSE +0 -0
- {zenml_nightly-0.83.0.dev20250618.dist-info → zenml_nightly-0.83.0.dev20250621.dist-info}/WHEEL +0 -0
- {zenml_nightly-0.83.0.dev20250618.dist-info → zenml_nightly-0.83.0.dev20250621.dist-info}/entry_points.txt +0 -0
@@ -22,15 +22,9 @@ To run this file locally, execute:
|
|
22
22
|
|
23
23
|
import logging
|
24
24
|
import os
|
25
|
-
import threading
|
26
|
-
import time
|
27
|
-
from asyncio import Lock, Semaphore, TimeoutError, wait_for
|
28
25
|
from asyncio.log import logger
|
29
|
-
from contextvars import ContextVar
|
30
|
-
from datetime import datetime, timedelta
|
31
26
|
from genericpath import isfile
|
32
|
-
from typing import Any, List
|
33
|
-
from uuid import uuid4
|
27
|
+
from typing import Any, List
|
34
28
|
|
35
29
|
from anyio import to_thread
|
36
30
|
from fastapi import FastAPI, HTTPException, Request
|
@@ -38,31 +32,25 @@ from fastapi.exceptions import RequestValidationError
|
|
38
32
|
from fastapi.responses import ORJSONResponse
|
39
33
|
from fastapi.staticfiles import StaticFiles
|
40
34
|
from fastapi.templating import Jinja2Templates
|
41
|
-
from starlette.middleware.base import (
|
42
|
-
BaseHTTPMiddleware,
|
43
|
-
RequestResponseEndpoint,
|
44
|
-
)
|
45
|
-
from starlette.middleware.cors import CORSMiddleware
|
46
35
|
from starlette.responses import (
|
47
36
|
FileResponse,
|
48
|
-
JSONResponse,
|
49
37
|
RedirectResponse,
|
50
|
-
Response,
|
51
38
|
)
|
52
|
-
from starlette.types import ASGIApp
|
53
39
|
|
54
40
|
import zenml
|
55
|
-
from zenml.analytics import source_context
|
56
41
|
from zenml.constants import (
|
57
42
|
API,
|
58
|
-
DEFAULT_ZENML_SERVER_REPORT_USER_ACTIVITY_TO_DB_SECONDS,
|
59
43
|
HEALTH,
|
44
|
+
READY,
|
60
45
|
)
|
61
|
-
from zenml.enums import AuthScheme
|
46
|
+
from zenml.enums import AuthScheme
|
62
47
|
from zenml.models import ServerDeploymentType
|
63
|
-
from zenml.
|
48
|
+
from zenml.service_connectors.service_connector_registry import (
|
49
|
+
service_connector_registry,
|
50
|
+
)
|
64
51
|
from zenml.zen_server.cloud_utils import send_pro_workspace_status_update
|
65
52
|
from zenml.zen_server.exceptions import error_detail
|
53
|
+
from zenml.zen_server.middleware import add_middlewares
|
66
54
|
from zenml.zen_server.routers import (
|
67
55
|
actions_endpoints,
|
68
56
|
artifact_endpoint,
|
@@ -101,28 +89,25 @@ from zenml.zen_server.routers import (
|
|
101
89
|
)
|
102
90
|
from zenml.zen_server.secure_headers import (
|
103
91
|
initialize_secure_headers,
|
104
|
-
secure_headers,
|
105
92
|
)
|
106
93
|
from zenml.zen_server.utils import (
|
94
|
+
cleanup_request_manager,
|
107
95
|
initialize_feature_gate,
|
108
96
|
initialize_memcache,
|
109
97
|
initialize_plugins,
|
110
98
|
initialize_rbac,
|
99
|
+
initialize_request_manager,
|
111
100
|
initialize_run_template_executor,
|
112
101
|
initialize_workload_manager,
|
113
102
|
initialize_zen_store,
|
114
|
-
is_user_request,
|
115
103
|
run_template_executor,
|
116
104
|
server_config,
|
117
|
-
|
105
|
+
start_event_loop_lag_monitor,
|
106
|
+
stop_event_loop_lag_monitor,
|
118
107
|
)
|
119
108
|
|
120
109
|
DASHBOARD_DIRECTORY = "dashboard"
|
121
110
|
|
122
|
-
request_ids: ContextVar[Optional[str]] = ContextVar(
|
123
|
-
"request_ids", default=None
|
124
|
-
)
|
125
|
-
|
126
111
|
|
127
112
|
def relative_path(rel: str) -> str:
|
128
113
|
"""Get the absolute path of a path relative to the ZenML server module.
|
@@ -143,12 +128,7 @@ app = FastAPI(
|
|
143
128
|
default_response_class=ORJSONResponse,
|
144
129
|
)
|
145
130
|
|
146
|
-
|
147
|
-
last_user_activity: datetime = utc_now()
|
148
|
-
last_user_activity_reported: datetime = last_user_activity + timedelta(
|
149
|
-
seconds=-DEFAULT_ZENML_SERVER_REPORT_USER_ACTIVITY_TO_DB_SECONDS
|
150
|
-
)
|
151
|
-
last_user_activity_lock = Lock()
|
131
|
+
add_middlewares(app)
|
152
132
|
|
153
133
|
|
154
134
|
# Customize the default request validation handler that comes with FastAPI
|
@@ -169,373 +149,8 @@ def validation_exception_handler(
|
|
169
149
|
return ORJSONResponse(error_detail(exc, ValueError), status_code=422)
|
170
150
|
|
171
151
|
|
172
|
-
class RequestBodyLimit(BaseHTTPMiddleware):
|
173
|
-
"""Limits the size of the request body."""
|
174
|
-
|
175
|
-
def __init__(self, app: ASGIApp, max_bytes: int) -> None:
|
176
|
-
"""Limits the size of the request body.
|
177
|
-
|
178
|
-
Args:
|
179
|
-
app: The FastAPI app.
|
180
|
-
max_bytes: The maximum size of the request body.
|
181
|
-
"""
|
182
|
-
super().__init__(app)
|
183
|
-
self.max_bytes = max_bytes
|
184
|
-
|
185
|
-
async def dispatch(
|
186
|
-
self, request: Request, call_next: RequestResponseEndpoint
|
187
|
-
) -> Response:
|
188
|
-
"""Limits the size of the request body.
|
189
|
-
|
190
|
-
Args:
|
191
|
-
request: The incoming request.
|
192
|
-
call_next: The next function to be called.
|
193
|
-
|
194
|
-
Returns:
|
195
|
-
The response to the request.
|
196
|
-
"""
|
197
|
-
if content_length := request.headers.get("content-length"):
|
198
|
-
if int(content_length) > self.max_bytes:
|
199
|
-
return Response(status_code=413) # Request Entity Too Large
|
200
|
-
|
201
|
-
try:
|
202
|
-
return await call_next(request)
|
203
|
-
except Exception:
|
204
|
-
logger.exception("An error occurred while processing the request")
|
205
|
-
return JSONResponse(
|
206
|
-
status_code=500,
|
207
|
-
content={"detail": "An unexpected error occurred."},
|
208
|
-
)
|
209
|
-
|
210
|
-
|
211
|
-
class RestrictFileUploadsMiddleware(BaseHTTPMiddleware):
|
212
|
-
"""Restrict file uploads to certain paths."""
|
213
|
-
|
214
|
-
def __init__(self, app: FastAPI, allowed_paths: Set[str]):
|
215
|
-
"""Restrict file uploads to certain paths.
|
216
|
-
|
217
|
-
Args:
|
218
|
-
app: The FastAPI app.
|
219
|
-
allowed_paths: The allowed paths.
|
220
|
-
"""
|
221
|
-
super().__init__(app)
|
222
|
-
self.allowed_paths = allowed_paths
|
223
|
-
|
224
|
-
async def dispatch(
|
225
|
-
self, request: Request, call_next: RequestResponseEndpoint
|
226
|
-
) -> Response:
|
227
|
-
"""Restrict file uploads to certain paths.
|
228
|
-
|
229
|
-
Args:
|
230
|
-
request: The incoming request.
|
231
|
-
call_next: The next function to be called.
|
232
|
-
|
233
|
-
Returns:
|
234
|
-
The response to the request.
|
235
|
-
"""
|
236
|
-
if request.method == "POST":
|
237
|
-
content_type = request.headers.get("content-type", "")
|
238
|
-
if (
|
239
|
-
"multipart/form-data" in content_type
|
240
|
-
and request.url.path not in self.allowed_paths
|
241
|
-
):
|
242
|
-
return JSONResponse(
|
243
|
-
status_code=403,
|
244
|
-
content={
|
245
|
-
"detail": "File uploads are not allowed on this endpoint."
|
246
|
-
},
|
247
|
-
)
|
248
|
-
|
249
|
-
try:
|
250
|
-
return await call_next(request)
|
251
|
-
except Exception:
|
252
|
-
logger.exception("An error occurred while processing the request")
|
253
|
-
return JSONResponse(
|
254
|
-
status_code=500,
|
255
|
-
content={"detail": "An unexpected error occurred."},
|
256
|
-
)
|
257
|
-
|
258
|
-
|
259
|
-
ALLOWED_FOR_FILE_UPLOAD: Set[str] = set()
|
260
|
-
|
261
|
-
|
262
|
-
@app.middleware("http")
|
263
|
-
async def track_last_user_activity(request: Request, call_next: Any) -> Any:
|
264
|
-
"""A middleware to track last user activity.
|
265
|
-
|
266
|
-
This middleware checks if the incoming request is a user request and
|
267
|
-
updates the last activity timestamp if it is.
|
268
|
-
|
269
|
-
Args:
|
270
|
-
request: The incoming request object.
|
271
|
-
call_next: A function that will receive the request as a parameter and
|
272
|
-
pass it to the corresponding path operation.
|
273
|
-
|
274
|
-
Returns:
|
275
|
-
The response to the request.
|
276
|
-
"""
|
277
|
-
global last_user_activity
|
278
|
-
global last_user_activity_reported
|
279
|
-
global last_user_activity_lock
|
280
|
-
|
281
|
-
now = utc_now()
|
282
|
-
|
283
|
-
try:
|
284
|
-
if is_user_request(request):
|
285
|
-
report_user_activity = False
|
286
|
-
async with last_user_activity_lock:
|
287
|
-
last_user_activity = now
|
288
|
-
if (
|
289
|
-
(now - last_user_activity_reported).total_seconds()
|
290
|
-
> DEFAULT_ZENML_SERVER_REPORT_USER_ACTIVITY_TO_DB_SECONDS
|
291
|
-
):
|
292
|
-
last_user_activity_reported = now
|
293
|
-
report_user_activity = True
|
294
|
-
|
295
|
-
if report_user_activity:
|
296
|
-
zen_store()._update_last_user_activity_timestamp(
|
297
|
-
last_user_activity=last_user_activity
|
298
|
-
)
|
299
|
-
except Exception as e:
|
300
|
-
logger.debug(
|
301
|
-
f"An unexpected error occurred while checking user activity: {e}"
|
302
|
-
)
|
303
|
-
|
304
|
-
try:
|
305
|
-
return await call_next(request)
|
306
|
-
except Exception:
|
307
|
-
logger.exception("An error occurred while processing the request")
|
308
|
-
return JSONResponse(
|
309
|
-
status_code=500,
|
310
|
-
content={"detail": "An unexpected error occurred."},
|
311
|
-
)
|
312
|
-
|
313
|
-
|
314
|
-
@app.middleware("http")
|
315
|
-
async def infer_source_context(request: Request, call_next: Any) -> Any:
|
316
|
-
"""A middleware to track the source of an event.
|
317
|
-
|
318
|
-
It extracts the source context from the header of incoming requests
|
319
|
-
and applies it to the ZenML source context on the API side. This way, the
|
320
|
-
outgoing analytics request can append it as an additional field.
|
321
|
-
|
322
|
-
Args:
|
323
|
-
request: the incoming request object.
|
324
|
-
call_next: a function that will receive the request as a parameter and
|
325
|
-
pass it to the corresponding path operation.
|
326
|
-
|
327
|
-
Returns:
|
328
|
-
the response to the request.
|
329
|
-
"""
|
330
|
-
try:
|
331
|
-
s = request.headers.get(
|
332
|
-
source_context.name,
|
333
|
-
default=SourceContextTypes.API.value,
|
334
|
-
)
|
335
|
-
source_context.set(SourceContextTypes(s))
|
336
|
-
except Exception as e:
|
337
|
-
logger.warning(
|
338
|
-
f"An unexpected error occurred while getting the source "
|
339
|
-
f"context: {e}"
|
340
|
-
)
|
341
|
-
source_context.set(SourceContextTypes.API)
|
342
|
-
|
343
|
-
try:
|
344
|
-
return await call_next(request)
|
345
|
-
except Exception:
|
346
|
-
logger.exception("An error occurred while processing the request")
|
347
|
-
return JSONResponse(
|
348
|
-
status_code=500,
|
349
|
-
content={"detail": "An unexpected error occurred."},
|
350
|
-
)
|
351
|
-
|
352
|
-
|
353
|
-
request_semaphore = Semaphore(server_config().thread_pool_size)
|
354
|
-
|
355
|
-
|
356
|
-
@app.middleware("http")
|
357
|
-
async def prevent_read_timeout(request: Request, call_next: Any) -> Any:
|
358
|
-
"""Prevent read timeout client errors.
|
359
|
-
|
360
|
-
Args:
|
361
|
-
request: The incoming request.
|
362
|
-
call_next: The next function to be called.
|
363
|
-
|
364
|
-
Returns:
|
365
|
-
The response to the request.
|
366
|
-
"""
|
367
|
-
# Only process the REST API requests because these are the ones that
|
368
|
-
# take the most time to complete.
|
369
|
-
if not request.url.path.startswith(API):
|
370
|
-
return await call_next(request)
|
371
|
-
|
372
|
-
server_request_timeout = server_config().server_request_timeout
|
373
|
-
|
374
|
-
active_threads = threading.active_count()
|
375
|
-
request_id = request_ids.get()
|
376
|
-
|
377
|
-
client_ip = request.client.host if request.client else "unknown"
|
378
|
-
method = request.method
|
379
|
-
url_path = request.url.path
|
380
|
-
|
381
|
-
logger.debug(
|
382
|
-
f"[{request_id}] API STATS - {method} {url_path} from {client_ip} "
|
383
|
-
f"QUEUED [ "
|
384
|
-
f"threads: {active_threads} "
|
385
|
-
f"]"
|
386
|
-
)
|
387
|
-
|
388
|
-
start_time = time.time()
|
389
|
-
|
390
|
-
try:
|
391
|
-
# Here we wait until a worker thread is available to process the
|
392
|
-
# request with a timeout value that is set to be lower than the
|
393
|
-
# what the client is willing to wait for (i.e. lower than the
|
394
|
-
# client's HTTP request timeout). The rationale is that we want to
|
395
|
-
# respond to the client before it times out and decides to retry the
|
396
|
-
# request (which would overwhelm the server).
|
397
|
-
await wait_for(
|
398
|
-
request_semaphore.acquire(),
|
399
|
-
timeout=server_request_timeout,
|
400
|
-
)
|
401
|
-
except TimeoutError:
|
402
|
-
end_time = time.time()
|
403
|
-
duration = (end_time - start_time) * 1000
|
404
|
-
active_threads = threading.active_count()
|
405
|
-
|
406
|
-
logger.debug(
|
407
|
-
f"[{request_id}] API STATS - {method} {url_path} from {client_ip} "
|
408
|
-
f"THROTTLED after {duration:.2f}ms [ "
|
409
|
-
f"threads: {active_threads} "
|
410
|
-
f"]"
|
411
|
-
)
|
412
|
-
|
413
|
-
# We return a 429 error, basically telling the client to slow down.
|
414
|
-
# For the client, the 429 error is more meaningful than a ReadTimeout
|
415
|
-
# error, because it also tells the client two additional things:
|
416
|
-
#
|
417
|
-
# 1. The server is alive.
|
418
|
-
# 2. The server hasn't processed the request, so even if the request
|
419
|
-
# is not idempotent, it's safe to retry it.
|
420
|
-
return JSONResponse(
|
421
|
-
{"error": "Server too busy. Please try again later."},
|
422
|
-
status_code=429,
|
423
|
-
)
|
424
|
-
|
425
|
-
duration = (time.time() - start_time) * 1000
|
426
|
-
active_threads = threading.active_count()
|
427
|
-
|
428
|
-
logger.debug(
|
429
|
-
f"[{request_id}] API STATS - {method} {url_path} from {client_ip} "
|
430
|
-
f"ACCEPTED after {duration:.2f}ms [ "
|
431
|
-
f"threads: {active_threads} "
|
432
|
-
f"]"
|
433
|
-
)
|
434
|
-
|
435
|
-
try:
|
436
|
-
return await call_next(request)
|
437
|
-
finally:
|
438
|
-
request_semaphore.release()
|
439
|
-
|
440
|
-
|
441
|
-
@app.middleware("http")
|
442
|
-
async def log_requests(request: Request, call_next: Any) -> Any:
|
443
|
-
"""Log requests to the ZenML server.
|
444
|
-
|
445
|
-
Args:
|
446
|
-
request: The incoming request object.
|
447
|
-
call_next: A function that will receive the request as a parameter and
|
448
|
-
pass it to the corresponding path operation.
|
449
|
-
|
450
|
-
Returns:
|
451
|
-
The response to the request.
|
452
|
-
"""
|
453
|
-
if not logger.isEnabledFor(logging.DEBUG):
|
454
|
-
return await call_next(request)
|
455
|
-
|
456
|
-
# Get active threads count
|
457
|
-
active_threads = threading.active_count()
|
458
|
-
|
459
|
-
request_id = request.headers.get("X-Request-ID", str(uuid4())[:8])
|
460
|
-
# Detect if the request comes from Python, Web UI or something else
|
461
|
-
if source := request.headers.get("User-Agent"):
|
462
|
-
source = source.split("/")[0]
|
463
|
-
request_id = f"{request_id}/{source}"
|
464
|
-
|
465
|
-
request_ids.set(request_id)
|
466
|
-
client_ip = request.client.host if request.client else "unknown"
|
467
|
-
method = request.method
|
468
|
-
url_path = request.url.path
|
469
|
-
|
470
|
-
logger.debug(
|
471
|
-
f"[{request_id}] API STATS - {method} {url_path} from {client_ip} "
|
472
|
-
f"RECEIVED [ "
|
473
|
-
f"threads: {active_threads} "
|
474
|
-
f"]"
|
475
|
-
)
|
476
|
-
|
477
|
-
start_time = time.time()
|
478
|
-
response = await call_next(request)
|
479
|
-
duration = (time.time() - start_time) * 1000
|
480
|
-
status_code = response.status_code
|
481
|
-
|
482
|
-
logger.debug(
|
483
|
-
f"[{request_id}] API STATS - {status_code} {method} {url_path} from "
|
484
|
-
f"{client_ip} took {duration:.2f}ms [ "
|
485
|
-
f"threads: {active_threads} "
|
486
|
-
f"]"
|
487
|
-
)
|
488
|
-
return response
|
489
|
-
|
490
|
-
|
491
|
-
app.add_middleware(
|
492
|
-
CORSMiddleware,
|
493
|
-
allow_origins=server_config().cors_allow_origins,
|
494
|
-
allow_credentials=True,
|
495
|
-
allow_methods=["*"],
|
496
|
-
allow_headers=["*"],
|
497
|
-
)
|
498
|
-
|
499
|
-
app.add_middleware(
|
500
|
-
RequestBodyLimit, max_bytes=server_config().max_request_body_size_in_bytes
|
501
|
-
)
|
502
|
-
app.add_middleware(
|
503
|
-
RestrictFileUploadsMiddleware, allowed_paths=ALLOWED_FOR_FILE_UPLOAD
|
504
|
-
)
|
505
|
-
|
506
|
-
|
507
|
-
@app.middleware("http")
|
508
|
-
async def set_secure_headers(request: Request, call_next: Any) -> Any:
|
509
|
-
"""Middleware to set secure headers.
|
510
|
-
|
511
|
-
Args:
|
512
|
-
request: The incoming request.
|
513
|
-
call_next: The next function to be called.
|
514
|
-
|
515
|
-
Returns:
|
516
|
-
The response with secure headers set.
|
517
|
-
"""
|
518
|
-
try:
|
519
|
-
response = await call_next(request)
|
520
|
-
except Exception:
|
521
|
-
logger.exception("An error occurred while processing the request")
|
522
|
-
response = JSONResponse(
|
523
|
-
status_code=500,
|
524
|
-
content={"detail": "An unexpected error occurred."},
|
525
|
-
)
|
526
|
-
|
527
|
-
# If the request is for the openAPI docs, don't set secure headers
|
528
|
-
if request.url.path.startswith("/docs") or request.url.path.startswith(
|
529
|
-
"/redoc"
|
530
|
-
):
|
531
|
-
return response
|
532
|
-
|
533
|
-
secure_headers().framework.fastapi(response)
|
534
|
-
return response
|
535
|
-
|
536
|
-
|
537
152
|
@app.on_event("startup")
|
538
|
-
def initialize() -> None:
|
153
|
+
async def initialize() -> None:
|
539
154
|
"""Initialize the ZenML server."""
|
540
155
|
cfg = server_config()
|
541
156
|
# Set the maximum number of worker threads
|
@@ -544,7 +159,9 @@ def initialize() -> None:
|
|
544
159
|
)
|
545
160
|
# IMPORTANT: these need to be run before the fastapi app starts, to avoid
|
546
161
|
# race conditions
|
162
|
+
await initialize_request_manager()
|
547
163
|
initialize_zen_store()
|
164
|
+
service_connector_registry.register_builtin_service_connectors()
|
548
165
|
initialize_rbac()
|
549
166
|
initialize_feature_gate()
|
550
167
|
initialize_workload_manager()
|
@@ -557,11 +174,17 @@ def initialize() -> None:
|
|
557
174
|
# ZenML server is running or to update the version and server URL.
|
558
175
|
send_pro_workspace_status_update()
|
559
176
|
|
177
|
+
if logger.isEnabledFor(logging.DEBUG):
|
178
|
+
start_event_loop_lag_monitor()
|
179
|
+
|
560
180
|
|
561
181
|
@app.on_event("shutdown")
|
562
|
-
def shutdown() -> None:
|
182
|
+
async def shutdown() -> None:
|
563
183
|
"""Shutdown the ZenML server."""
|
184
|
+
if logger.isEnabledFor(logging.DEBUG):
|
185
|
+
stop_event_loop_lag_monitor()
|
564
186
|
run_template_executor().shutdown(wait=True)
|
187
|
+
await cleanup_request_manager()
|
565
188
|
|
566
189
|
|
567
190
|
DASHBOARD_REDIRECT_URL = None
|
@@ -595,6 +218,18 @@ async def health() -> str:
|
|
595
218
|
return "OK"
|
596
219
|
|
597
220
|
|
221
|
+
# Basic Ready Endpoint
|
222
|
+
@app.head(READY, include_in_schema=False)
|
223
|
+
@app.get(READY)
|
224
|
+
async def ready() -> str:
|
225
|
+
"""Get readiness status of the server.
|
226
|
+
|
227
|
+
Returns:
|
228
|
+
String representing the readiness status of the server.
|
229
|
+
"""
|
230
|
+
return "OK"
|
231
|
+
|
232
|
+
|
598
233
|
templates = Jinja2Templates(directory=relative_path(DASHBOARD_DIRECTORY))
|
599
234
|
|
600
235
|
|
@@ -0,0 +1,138 @@
|
|
1
|
+
"""Split up step configurations [3d7e39f3ac92].
|
2
|
+
|
3
|
+
Revision ID: 3d7e39f3ac92
|
4
|
+
Revises: 857843db1bcf
|
5
|
+
Create Date: 2025-06-17 17:45:31.702617
|
6
|
+
|
7
|
+
"""
|
8
|
+
|
9
|
+
import json
|
10
|
+
import uuid
|
11
|
+
|
12
|
+
import sqlalchemy as sa
|
13
|
+
import sqlmodel
|
14
|
+
from alembic import op
|
15
|
+
from sqlalchemy.dialects import mysql
|
16
|
+
|
17
|
+
from zenml.utils.time_utils import utc_now
|
18
|
+
|
19
|
+
# revision identifiers, used by Alembic.
|
20
|
+
revision = "3d7e39f3ac92"
|
21
|
+
down_revision = "857843db1bcf"
|
22
|
+
branch_labels = None
|
23
|
+
depends_on = None
|
24
|
+
|
25
|
+
|
26
|
+
def upgrade() -> None:
|
27
|
+
"""Upgrade database schema and/or data, creating a new revision."""
|
28
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
29
|
+
op.create_table(
|
30
|
+
"step_configuration",
|
31
|
+
sa.Column("id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
|
32
|
+
sa.Column("created", sa.DateTime(), nullable=False),
|
33
|
+
sa.Column("updated", sa.DateTime(), nullable=False),
|
34
|
+
sa.Column("index", sa.Integer(), nullable=False),
|
35
|
+
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
36
|
+
sa.Column(
|
37
|
+
"config",
|
38
|
+
sa.String(length=16777215).with_variant(mysql.MEDIUMTEXT, "mysql"),
|
39
|
+
nullable=False,
|
40
|
+
),
|
41
|
+
sa.Column(
|
42
|
+
"deployment_id", sqlmodel.sql.sqltypes.GUID(), nullable=False
|
43
|
+
),
|
44
|
+
sa.ForeignKeyConstraint(
|
45
|
+
["deployment_id"],
|
46
|
+
["pipeline_deployment.id"],
|
47
|
+
name="fk_step_configuration_deployment_id_pipeline_deployment",
|
48
|
+
ondelete="CASCADE",
|
49
|
+
),
|
50
|
+
sa.PrimaryKeyConstraint("id"),
|
51
|
+
sa.UniqueConstraint(
|
52
|
+
"deployment_id", "name", name="unique_step_name_for_deployment"
|
53
|
+
),
|
54
|
+
)
|
55
|
+
with op.batch_alter_table("pipeline_deployment", schema=None) as batch_op:
|
56
|
+
batch_op.add_column(
|
57
|
+
sa.Column("step_count", sa.Integer(), nullable=True)
|
58
|
+
)
|
59
|
+
|
60
|
+
# Migrate existing step configurations
|
61
|
+
connection = op.get_bind()
|
62
|
+
meta = sa.MetaData()
|
63
|
+
meta.reflect(
|
64
|
+
bind=connection, only=("pipeline_deployment", "step_configuration")
|
65
|
+
)
|
66
|
+
pipeline_deployment_table = sa.Table("pipeline_deployment", meta)
|
67
|
+
step_configuration_table = sa.Table("step_configuration", meta)
|
68
|
+
|
69
|
+
step_configurations_to_insert = []
|
70
|
+
deployment_updates = []
|
71
|
+
|
72
|
+
for deployment_id, step_configurations_json in connection.execute(
|
73
|
+
sa.select(
|
74
|
+
pipeline_deployment_table.c.id,
|
75
|
+
pipeline_deployment_table.c.step_configurations,
|
76
|
+
)
|
77
|
+
):
|
78
|
+
step_configurations = json.loads(step_configurations_json)
|
79
|
+
|
80
|
+
step_count = len(step_configurations)
|
81
|
+
deployment_updates.append(
|
82
|
+
{
|
83
|
+
"id_": deployment_id,
|
84
|
+
"step_count": step_count,
|
85
|
+
}
|
86
|
+
)
|
87
|
+
|
88
|
+
for index, (step_name, step_config) in enumerate(
|
89
|
+
step_configurations.items()
|
90
|
+
):
|
91
|
+
now = utc_now()
|
92
|
+
step_configurations_to_insert.append(
|
93
|
+
{
|
94
|
+
"id": str(uuid.uuid4()).replace("-", ""),
|
95
|
+
"created": now,
|
96
|
+
"updated": now,
|
97
|
+
"index": index,
|
98
|
+
"name": step_name,
|
99
|
+
"config": json.dumps(step_config),
|
100
|
+
"deployment_id": deployment_id,
|
101
|
+
}
|
102
|
+
)
|
103
|
+
|
104
|
+
op.bulk_insert(
|
105
|
+
step_configuration_table, rows=step_configurations_to_insert
|
106
|
+
)
|
107
|
+
if deployment_updates:
|
108
|
+
connection.execute(
|
109
|
+
sa.update(pipeline_deployment_table).where(
|
110
|
+
pipeline_deployment_table.c.id == sa.bindparam("id_")
|
111
|
+
),
|
112
|
+
deployment_updates,
|
113
|
+
)
|
114
|
+
|
115
|
+
with op.batch_alter_table("pipeline_deployment", schema=None) as batch_op:
|
116
|
+
batch_op.alter_column(
|
117
|
+
"step_count", existing_type=sa.Integer(), nullable=False
|
118
|
+
)
|
119
|
+
batch_op.drop_column("step_configurations")
|
120
|
+
|
121
|
+
# ### end Alembic commands ###
|
122
|
+
|
123
|
+
|
124
|
+
def downgrade() -> None:
|
125
|
+
"""Downgrade database schema and/or data back to the previous revision."""
|
126
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
127
|
+
with op.batch_alter_table("pipeline_deployment", schema=None) as batch_op:
|
128
|
+
batch_op.add_column(
|
129
|
+
sa.Column(
|
130
|
+
"step_configurations",
|
131
|
+
sa.VARCHAR(length=16777215),
|
132
|
+
nullable=False,
|
133
|
+
)
|
134
|
+
)
|
135
|
+
batch_op.drop_column("step_count")
|
136
|
+
|
137
|
+
op.drop_table("step_configuration")
|
138
|
+
# ### end Alembic commands ###
|