prefect 3.9.2.dev1__py3-none-any.whl → 3.9.2.dev2__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.
- prefect/_build_info.py +3 -3
- prefect/cli/artifact.py +2 -2
- prefect/server/AGENTS.md +1 -0
- prefect/server/api/server.py +61 -7
- prefect/server/utilities/server.py +61 -0
- {prefect-3.9.2.dev1.dist-info → prefect-3.9.2.dev2.dist-info}/METADATA +1 -1
- {prefect-3.9.2.dev1.dist-info → prefect-3.9.2.dev2.dist-info}/RECORD +10 -10
- {prefect-3.9.2.dev1.dist-info → prefect-3.9.2.dev2.dist-info}/WHEEL +0 -0
- {prefect-3.9.2.dev1.dist-info → prefect-3.9.2.dev2.dist-info}/entry_points.txt +0 -0
- {prefect-3.9.2.dev1.dist-info → prefect-3.9.2.dev2.dist-info}/licenses/LICENSE +0 -0
prefect/_build_info.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Generated by versioningit
|
|
2
|
-
__version__ = "3.9.2.
|
|
3
|
-
__build_date__ = "2026-11-
|
|
4
|
-
__git_commit__ = "
|
|
2
|
+
__version__ = "3.9.2.dev2"
|
|
3
|
+
__build_date__ = "2026-11-02 08:16:54.718332+00:00"
|
|
4
|
+
__git_commit__ = "da9d9dc15f647f10135a8ff79ce93c4bdb2dbf6a"
|
|
5
5
|
__dirty__ = False
|
prefect/cli/artifact.py
CHANGED
|
@@ -77,7 +77,7 @@ async def list_artifacts(
|
|
|
77
77
|
if output and output.lower() == "json":
|
|
78
78
|
artifacts_json = [artifact.model_dump(mode="json") for artifact in artifacts]
|
|
79
79
|
json_output = orjson.dumps(artifacts_json, option=orjson.OPT_INDENT_2).decode()
|
|
80
|
-
_cli.console.print(json_output, soft_wrap=True)
|
|
80
|
+
_cli.console.print(json_output, soft_wrap=True, markup=False, emoji=False)
|
|
81
81
|
else:
|
|
82
82
|
table = Table(
|
|
83
83
|
title="Artifacts",
|
|
@@ -181,7 +181,7 @@ async def inspect(
|
|
|
181
181
|
json_output = orjson.dumps(
|
|
182
182
|
artifacts_json, option=orjson.OPT_INDENT_2
|
|
183
183
|
).decode()
|
|
184
|
-
_cli.console.print(json_output, soft_wrap=True)
|
|
184
|
+
_cli.console.print(json_output, soft_wrap=True, markup=False, emoji=False)
|
|
185
185
|
else:
|
|
186
186
|
_cli.console.print(Pretty(artifacts_json))
|
|
187
187
|
|
prefect/server/AGENTS.md
CHANGED
|
@@ -73,6 +73,7 @@ alembic_revision("description") # Create a new migration
|
|
|
73
73
|
- **`_segment_task_runs_for_upsert` must keep emitting contiguous blocks of its input.** That property, not the batch key, is what preserves the lock ordering above, so a new condition may only *close* a segment earlier — never reorder rows. Each existing condition guards a failure that is silent on at least one dialect; see its docstring and `_fillable_columns` before relaxing one. Absent keys are `NULL`-filled and restored with `coalesce(excluded.col, task_run.col)`, applied to exactly the columns that were filled — a payload asking to write a real `NULL` keeps a plain `excluded.col`, and the batch splits rather than let one statement mean both. This is also why `flow_run_id` must stay out of `_fillable_columns`: coalescing it would break detach.
|
|
74
74
|
|
|
75
75
|
- **`docket.add()` in monitor loops must pass a per-entity `key=` to prevent duplicate enqueues.** Without it, repeated iterations over the same pending entity enqueue duplicate tasks — causing duplicate state transitions. Format: `"<service-name>:<entity-id>"`, e.g., `"mark-flow-run-late:{run.id}"` in `services/late_runs.py`.
|
|
76
|
+
Keyed `add()` can suppress later enqueues while earlier work remains known to Docket, including stranded work awaiting redelivery after a Redis disruption. Use `replace()` only when superseding the pending schedule and arguments is safe (see `schedule_readiness_update` in `api/workers.py`). Choose keys by logical work: independent updates that must both run need distinct keys, while updates intended to supersede one another share a key. Replacement does not synchronously cancel already-running work; `redelivery_timeout` controls reclamation eligibility, not key expiration.
|
|
76
77
|
|
|
77
78
|
- **`TaskRunRecorder` and `EventPersister` share a buffered-consumer shape; keep their failure handling in sync.** Both (`services/task_run_recorder.py`, `events/services/event_persister.py`) queue messages and flush from a never-awaited `flush_periodically` task, so any exception escaping that loop silently stops flushing for the life of the service (#22441, #23308). Database drivers can raise `CancelledError` during outages, so the loop exits on cancellation only after the shutdown path sets its `stopping` flag. Mirror retry/drop/cancellation fixes in both.
|
|
78
79
|
|
prefect/server/api/server.py
CHANGED
|
@@ -11,7 +11,9 @@ import contextlib
|
|
|
11
11
|
import gc
|
|
12
12
|
import hmac
|
|
13
13
|
import importlib.metadata
|
|
14
|
+
import json
|
|
14
15
|
import logging
|
|
16
|
+
import math
|
|
15
17
|
import mimetypes
|
|
16
18
|
import os
|
|
17
19
|
import random
|
|
@@ -292,16 +294,34 @@ async def validation_exception_handler(
|
|
|
292
294
|
"""Provide a detailed message for request validation errors."""
|
|
293
295
|
return JSONResponse(
|
|
294
296
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
295
|
-
content=
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
297
|
+
content=_json_safe(
|
|
298
|
+
jsonable_encoder(
|
|
299
|
+
{
|
|
300
|
+
"exception_message": "Invalid request received.",
|
|
301
|
+
"exception_detail": exc.errors(),
|
|
302
|
+
"request_body": exc.body,
|
|
303
|
+
}
|
|
304
|
+
)
|
|
301
305
|
),
|
|
302
306
|
)
|
|
303
307
|
|
|
304
308
|
|
|
309
|
+
def _json_safe(value: Any) -> Any:
|
|
310
|
+
"""
|
|
311
|
+
Replace values that cannot be rendered as strict JSON, such as `NaN` or strings
|
|
312
|
+
containing unpaired surrogates, so that invalid input can be echoed back.
|
|
313
|
+
"""
|
|
314
|
+
if isinstance(value, float) and not math.isfinite(value):
|
|
315
|
+
return json.dumps(value)
|
|
316
|
+
if isinstance(value, str):
|
|
317
|
+
return value.encode("utf-8", "backslashreplace").decode("utf-8")
|
|
318
|
+
if isinstance(value, dict):
|
|
319
|
+
return {_json_safe(k): _json_safe(v) for k, v in value.items()}
|
|
320
|
+
if isinstance(value, list):
|
|
321
|
+
return [_json_safe(v) for v in value]
|
|
322
|
+
return value
|
|
323
|
+
|
|
324
|
+
|
|
305
325
|
async def integrity_exception_handler(request: Request, exc: Exception) -> JSONResponse:
|
|
306
326
|
"""Capture database integrity errors."""
|
|
307
327
|
logger.error("Encountered exception in request:", exc_info=True)
|
|
@@ -317,7 +337,20 @@ async def integrity_exception_handler(request: Request, exc: Exception) -> JSONR
|
|
|
317
337
|
)
|
|
318
338
|
|
|
319
339
|
|
|
340
|
+
def is_invalid_data_exception(exc: Exception) -> bool:
|
|
341
|
+
"""
|
|
342
|
+
Whether the database rejected a value as invalid (SQLSTATE class 22, "data
|
|
343
|
+
exception"), e.g. a NUL character or `NaN` in a PostgreSQL JSON value.
|
|
344
|
+
"""
|
|
345
|
+
orig = exc.orig if isinstance(exc, sqlalchemy.exc.DBAPIError) else exc
|
|
346
|
+
sqlstate = getattr(orig, "sqlstate", None)
|
|
347
|
+
return isinstance(sqlstate, str) and sqlstate.startswith("22")
|
|
348
|
+
|
|
349
|
+
|
|
320
350
|
def is_client_retryable_exception(exc: Exception) -> bool:
|
|
351
|
+
if is_invalid_data_exception(exc):
|
|
352
|
+
return False
|
|
353
|
+
|
|
321
354
|
if isinstance(exc, sqlalchemy.exc.OperationalError) and isinstance(
|
|
322
355
|
exc.orig, sqlite3.OperationalError
|
|
323
356
|
):
|
|
@@ -399,8 +432,29 @@ async def custom_internal_exception_handler(
|
|
|
399
432
|
"""
|
|
400
433
|
Log a detailed exception for internal server errors before returning.
|
|
401
434
|
|
|
402
|
-
Send 503 for errors clients can retry on.
|
|
435
|
+
Send 422 for values the database rejected and 503 for errors clients can retry on.
|
|
403
436
|
"""
|
|
437
|
+
if is_invalid_data_exception(exc):
|
|
438
|
+
logger.warning("Database rejected a value in request:", exc_info=True)
|
|
439
|
+
return JSONResponse(
|
|
440
|
+
content={
|
|
441
|
+
"exception_message": "Invalid request received.",
|
|
442
|
+
"exception_detail": [
|
|
443
|
+
{
|
|
444
|
+
"type": "value_error",
|
|
445
|
+
"loc": ["body"],
|
|
446
|
+
"msg": (
|
|
447
|
+
"The database rejected a value in the request. This "
|
|
448
|
+
"usually means a string contains a NUL character or a "
|
|
449
|
+
"JSON value contains NaN or Infinity. See server logs "
|
|
450
|
+
"for details."
|
|
451
|
+
),
|
|
452
|
+
}
|
|
453
|
+
],
|
|
454
|
+
},
|
|
455
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
456
|
+
)
|
|
457
|
+
|
|
404
458
|
if is_client_retryable_exception(exc):
|
|
405
459
|
if PREFECT_API_LOG_RETRYABLE_ERRORS.value():
|
|
406
460
|
logger.error("Encountered retryable exception in request:", exc_info=True)
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
Utilities for the Prefect REST API server.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
import re
|
|
5
6
|
from collections.abc import Coroutine, Sequence
|
|
6
7
|
from contextlib import AsyncExitStack
|
|
7
8
|
from typing import TYPE_CHECKING, Any, Callable, get_type_hints
|
|
8
9
|
|
|
9
10
|
from fastapi import APIRouter, Request, Response, status
|
|
11
|
+
from fastapi.exceptions import RequestValidationError
|
|
10
12
|
from fastapi.routing import APIRoute
|
|
11
13
|
from starlette.routing import BaseRoute
|
|
12
14
|
|
|
@@ -38,6 +40,62 @@ def method_paths_from_routes(routes: Sequence[BaseRoute]) -> set[str]:
|
|
|
38
40
|
return method_paths
|
|
39
41
|
|
|
40
42
|
|
|
43
|
+
# A JSON `\uD800`-`\uDFFF` escape, or the raw bytes of a surrogate, which
|
|
44
|
+
# `json.loads` decodes with `surrogatepass`
|
|
45
|
+
_SURROGATE_PATTERN = re.compile(rb"\\u[dD][89a-fA-F]|\xed[\xa0-\xbf]")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _find_unpaired_surrogate(
|
|
49
|
+
value: Any, loc: tuple[str | int, ...]
|
|
50
|
+
) -> tuple[str | int, ...] | None:
|
|
51
|
+
"""Return the location of the first string that is not encodable as UTF-8."""
|
|
52
|
+
if isinstance(value, str):
|
|
53
|
+
try:
|
|
54
|
+
value.encode("utf-8")
|
|
55
|
+
except UnicodeEncodeError:
|
|
56
|
+
return loc
|
|
57
|
+
elif isinstance(value, dict):
|
|
58
|
+
for key, item in value.items():
|
|
59
|
+
if _find_unpaired_surrogate(key, loc) is not None:
|
|
60
|
+
return loc
|
|
61
|
+
if (found := _find_unpaired_surrogate(item, (*loc, key))) is not None:
|
|
62
|
+
return found
|
|
63
|
+
elif isinstance(value, list):
|
|
64
|
+
for index, item in enumerate(value):
|
|
65
|
+
if (found := _find_unpaired_surrogate(item, (*loc, index))) is not None:
|
|
66
|
+
return found
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
async def _reject_unpaired_surrogates(request: Request) -> None:
|
|
71
|
+
"""
|
|
72
|
+
Reject JSON bodies containing unpaired surrogates (e.g. `"\\ud800"`), which
|
|
73
|
+
cannot be encoded as UTF-8 and so can neither be stored nor returned.
|
|
74
|
+
"""
|
|
75
|
+
if "json" not in request.headers.get("content-type", "application/json"):
|
|
76
|
+
return
|
|
77
|
+
if not _SURROGATE_PATTERN.search(await request.body()):
|
|
78
|
+
return
|
|
79
|
+
try:
|
|
80
|
+
body = await request.json()
|
|
81
|
+
except ValueError:
|
|
82
|
+
# Leave reporting malformed JSON to FastAPI
|
|
83
|
+
return
|
|
84
|
+
loc = _find_unpaired_surrogate(body, ("body",))
|
|
85
|
+
if loc is not None:
|
|
86
|
+
raise RequestValidationError(
|
|
87
|
+
[
|
|
88
|
+
{
|
|
89
|
+
"type": "string_unicode",
|
|
90
|
+
"loc": loc,
|
|
91
|
+
"msg": "String should not contain unpaired surrogates",
|
|
92
|
+
"input": None,
|
|
93
|
+
}
|
|
94
|
+
],
|
|
95
|
+
body=body,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
41
99
|
class PrefectAPIRoute(APIRoute):
|
|
42
100
|
"""
|
|
43
101
|
A FastAPIRoute class which attaches an async stack to requests that exits before
|
|
@@ -55,6 +113,9 @@ class PrefectAPIRoute(APIRoute):
|
|
|
55
113
|
|
|
56
114
|
async def handle_response_scoped_depends(request: Request) -> Response:
|
|
57
115
|
# Create a new stack scoped to exit before the response is returned
|
|
116
|
+
if self.body_field is not None:
|
|
117
|
+
await _reject_unpaired_surrogates(request)
|
|
118
|
+
|
|
58
119
|
response = None
|
|
59
120
|
async with AsyncExitStack() as stack:
|
|
60
121
|
request.state.response_scoped_stack = stack
|
|
@@ -2,7 +2,7 @@ prefect/.prefectignore,sha256=awSprvKT0vI8a64mEOLrMxhxqcO-b0ERQeYpA2rNKVQ,390
|
|
|
2
2
|
prefect/AGENTS.md,sha256=SRVqK72XfJPQTRQzfD6-pR04qFZJjZr7nii7W058uJg,13934
|
|
3
3
|
prefect/__init__.py,sha256=FtzUynR70uuE9WgVABo7cZkGGeNdL8Q1gJVTtmqvpyg,6772
|
|
4
4
|
prefect/__main__.py,sha256=WFjw3kaYJY6pOTA7WDOgqjsz8zUEUZHCcj3P5wyVa-g,66
|
|
5
|
-
prefect/_build_info.py,sha256=
|
|
5
|
+
prefect/_build_info.py,sha256=Gv89dSXDpaBbnWJ7V0Wb6WjIfgOkxEURnR1HEFRmYqo,185
|
|
6
6
|
prefect/_flow_run_suspension.py,sha256=5zTTB7ZIBHzoS0pVrhNn23-9hK51qZ3CQA6C-azluC0,4144
|
|
7
7
|
prefect/agent.py,sha256=dPvG1jDGD5HSH7aM2utwtk6RaJ9qg13XjkA0lAIgQmY,287
|
|
8
8
|
prefect/artifacts.py,sha256=ydlAad-FUepGUvTzmDkSmZLFRdMHohqVhwKnufJkeOw,23955
|
|
@@ -164,7 +164,7 @@ prefect/cli/_transfer_utils.py,sha256=EkiTJ1zGN3bzLC0fx7dHOVRffIWuwZLbGFRKPO2elc
|
|
|
164
164
|
prefect/cli/_utilities.py,sha256=wD2-awfndmqd1la_eEJ0Qfa9xUvv8AtmF9noghu6Xg8,3289
|
|
165
165
|
prefect/cli/_worker_utils.py,sha256=uzZP5JUzTohkvTXMqjrtrq0Bxnw-bYkoXrbomqzD6lQ,5423
|
|
166
166
|
prefect/cli/api.py,sha256=aLP-d96HcHoHOzGN5nq7luM7IhMWMyI9aRcTJeYteJ0,9479
|
|
167
|
-
prefect/cli/artifact.py,sha256=
|
|
167
|
+
prefect/cli/artifact.py,sha256=EnKEuXs-pLpDRQozRCwcodfvwXt99gIs_fBCzGYlutw,8352
|
|
168
168
|
prefect/cli/automation.py,sha256=t633ggd45EVsb9lIpgvv-Fi1NhZ54KDu2oRMiXGafOQ,20673
|
|
169
169
|
prefect/cli/block.py,sha256=dLzyArboOJMlftY55gcGBvc3Fb6lK3qTTsFbaP3Dx1A,21703
|
|
170
170
|
prefect/cli/concurrency_limit.py,sha256=g9FkCnGsLrUb-egTQVUrzdJXz_qqQXudcBmNweiEsHM,7381
|
|
@@ -381,7 +381,7 @@ prefect/runtime/__init__.py,sha256=JswiTlYRup2zXOYu8AqJ7czKtgcw9Kxo0tTbS6aWCqY,4
|
|
|
381
381
|
prefect/runtime/deployment.py,sha256=jYD-jkbb1dAl0wQuiY5mulVTqAEnF4xE3D5Iard6yms,5552
|
|
382
382
|
prefect/runtime/flow_run.py,sha256=dwMF9UeBuC-sHnMltXRnA1TftOOjAn52B_P-TyDuaY4,10849
|
|
383
383
|
prefect/runtime/task_run.py,sha256=zYBSs7QrAu7c2IjKomRzPXKyIXrjqclMTMrco-dwyOw,4212
|
|
384
|
-
prefect/server/AGENTS.md,sha256=
|
|
384
|
+
prefect/server/AGENTS.md,sha256=H6L2sXglmiuHr2_tPa77NaTxuUOrWM5PChcVH2VQIRE,15526
|
|
385
385
|
prefect/server/__init__.py,sha256=kW3pOfJv1zX6OIsTPbIrezVFXJpQJsqISQYmY_8ouKo,611
|
|
386
386
|
prefect/server/collection_blocks_data.json,sha256=63Jz7z6sO6jgL1Pwvi5h4R48VsAyMEln3zPvqTlIP0s,285092
|
|
387
387
|
prefect/server/exceptions.py,sha256=zfbfOdF-PsqnRUqxUXJ__zD905XJx1egFBrrUbIfkRA,735
|
|
@@ -413,7 +413,7 @@ prefect/server/api/observed_states.py,sha256=zKe3qfNvlGwP4t6dvQqTEsg9qJHkNU1A0sB
|
|
|
413
413
|
prefect/server/api/root.py,sha256=c_av5N4MkjSfFn1fI9F9Rpn6O_haBd6mshoF8OTvNqo,1358
|
|
414
414
|
prefect/server/api/run_history.py,sha256=OR8YIP7PbviRUyehnN21KywFbuGpAt_VOFgNo2O7kcc,5428
|
|
415
415
|
prefect/server/api/saved_searches.py,sha256=tGBS8p6DjFOzwEkRhMKdxQuT900lNqA7Rr3walYuv5w,3232
|
|
416
|
-
prefect/server/api/server.py,sha256=
|
|
416
|
+
prefect/server/api/server.py,sha256=FyM-gBhp6anKsoo7K9DKng9jXB-f-FBxrXHnhP7swIo,47034
|
|
417
417
|
prefect/server/api/task_run_states.py,sha256=ZZfxj8PyVduF5RsnhVjJluDCHiIqr78WNW34NBscz2c,1651
|
|
418
418
|
prefect/server/api/task_runs.py,sha256=Y7FOYaiQnrC74YtYSFd33SEpE99Srz_CQ1-IgK_P5BM,14877
|
|
419
419
|
prefect/server/api/task_workers.py,sha256=IncrNCj2hk_QAcYt6cOWRgwPNnfwhLXEapeptYQ0Yi8,1068
|
|
@@ -1111,7 +1111,7 @@ prefect/server/utilities/http.py,sha256=f2AbXFSi9h003SsrSEgP750Sg5iu163NZOFGd05C
|
|
|
1111
1111
|
prefect/server/utilities/leasing.py,sha256=dmvWx1pHXOp1HkItan8Evw2DlN567LhgSsd-fZpnZs4,2468
|
|
1112
1112
|
prefect/server/utilities/names.py,sha256=OMvK06ERA6fF3SfQak26vzad9pGq6kWbs_BQN9Mk36s,121
|
|
1113
1113
|
prefect/server/utilities/postgres_listener.py,sha256=gViMm-NrHU10fa1f2dhGB1dKSP14TAcNkzXG0r0gd8s,10551
|
|
1114
|
-
prefect/server/utilities/server.py,sha256=
|
|
1114
|
+
prefect/server/utilities/server.py,sha256=UC6jTzxCeAQesXKbiczg_XOtzEHxhvYRobYak_osj5c,5748
|
|
1115
1115
|
prefect/server/utilities/subscriptions.py,sha256=vY5veRVsNlTHn_oucdHpykZJQldqdNOKCGF7b84NI78,5629
|
|
1116
1116
|
prefect/server/utilities/text_search_parser.py,sha256=kNNuw_KIyYhs_bQz81wHTtNczWdmIcyE60fG4DyzGew,4472
|
|
1117
1117
|
prefect/server/utilities/user_templates.py,sha256=bhaSqJTfTXJhnEYfLEr-d6ITA4qBCA2X7cJ0jICiiHU,6278
|
|
@@ -1240,8 +1240,8 @@ prefect/workers/_worker_channel/_protocol.py,sha256=GpCNh1o3qmmqHA_UOOTge1QVC6IR
|
|
|
1240
1240
|
prefect/workers/_worker_channel/_state.py,sha256=eQTFZtAVDZH1vVWps3SdeY6aW3qu2wx1UKYQXK3AyuE,5369
|
|
1241
1241
|
prefect/workers/_worker_channel/_sync.py,sha256=G5G8_UaQYbeLebi5Mb1Z_KgGfXfyjXo5uT9la5_zwqY,14984
|
|
1242
1242
|
prefect/workers/_worker_channel/_transport.py,sha256=6VXxiZKIu0rqImlif14ojoAL3UGOBTmNpnlYxNggaLE,9423
|
|
1243
|
-
prefect-3.9.2.
|
|
1244
|
-
prefect-3.9.2.
|
|
1245
|
-
prefect-3.9.2.
|
|
1246
|
-
prefect-3.9.2.
|
|
1247
|
-
prefect-3.9.2.
|
|
1243
|
+
prefect-3.9.2.dev2.dist-info/METADATA,sha256=t4EWiwzxBuUQkfctYSblYFBL73P2gVJApEKF7x2HU7o,14407
|
|
1244
|
+
prefect-3.9.2.dev2.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
1245
|
+
prefect-3.9.2.dev2.dist-info/entry_points.txt,sha256=HlY8up83iIq2vU2r33a0qSis4eOFSyb1mRH4l7Xt9X8,126
|
|
1246
|
+
prefect-3.9.2.dev2.dist-info/licenses/LICENSE,sha256=MCxsn8osAkzfxKC4CC_dLcUkU8DZLkyihZ8mGs3Ah3Q,11357
|
|
1247
|
+
prefect-3.9.2.dev2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|