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 CHANGED
@@ -1,5 +1,5 @@
1
1
  # Generated by versioningit
2
- __version__ = "3.9.2.dev1"
3
- __build_date__ = "2026-11-01 08:12:52.684608+00:00"
4
- __git_commit__ = "2b76fb3da359c20fc5fa4e4db615a6fc76330b1e"
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
 
@@ -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=jsonable_encoder(
296
- {
297
- "exception_message": "Invalid request received.",
298
- "exception_detail": exc.errors(),
299
- "request_body": exc.body,
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: prefect
3
- Version: 3.9.2.dev1
3
+ Version: 3.9.2.dev2
4
4
  Summary: Workflow orchestration and management.
5
5
  Project-URL: Changelog, https://github.com/PrefectHQ/prefect/releases
6
6
  Project-URL: Documentation, https://www.prefect.io/docs
@@ -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=50oMSKG2QlBjitsl1q_f6H4xrW_BRkY2Ia8B5fB1SwY,185
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=v8LmV6DRYPzWJ8DIqa5lmtp5HAMRCECaz-Jo8IajpOs,8298
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=KTJxS6fHBu-e2pnyVaOg_lBy7lc8__ien_XCaa76gZI,14944
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=V1SE92sKlYydaTYsOajzx2h7tUbtcsOYOqOdG3puabM,44956
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=tgPY97lLJakZqdzcxDzd9-ZNkAylcv77aegYsQ1mZNM,3635
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.dev1.dist-info/METADATA,sha256=9LdlybTCTWoi5B9Csd1vEddYfNZ3YkkHSCntGXswIHc,14407
1244
- prefect-3.9.2.dev1.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
1245
- prefect-3.9.2.dev1.dist-info/entry_points.txt,sha256=HlY8up83iIq2vU2r33a0qSis4eOFSyb1mRH4l7Xt9X8,126
1246
- prefect-3.9.2.dev1.dist-info/licenses/LICENSE,sha256=MCxsn8osAkzfxKC4CC_dLcUkU8DZLkyihZ8mGs3Ah3Q,11357
1247
- prefect-3.9.2.dev1.dist-info/RECORD,,
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,,