prefect-client 3.1.5__py3-none-any.whl → 3.1.6__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/__init__.py +3 -0
- prefect/_internal/compatibility/migration.py +1 -1
- prefect/_internal/concurrency/api.py +52 -52
- prefect/_internal/concurrency/calls.py +59 -35
- prefect/_internal/concurrency/cancellation.py +34 -18
- prefect/_internal/concurrency/event_loop.py +7 -6
- prefect/_internal/concurrency/threads.py +41 -33
- prefect/_internal/concurrency/waiters.py +28 -21
- prefect/_internal/pydantic/v1_schema.py +2 -2
- prefect/_internal/pydantic/v2_schema.py +10 -9
- prefect/_internal/schemas/bases.py +9 -7
- prefect/_internal/schemas/validators.py +2 -1
- prefect/_version.py +3 -3
- prefect/automations.py +53 -47
- prefect/blocks/abstract.py +12 -10
- prefect/blocks/core.py +4 -2
- prefect/cache_policies.py +11 -11
- prefect/client/__init__.py +3 -1
- prefect/client/base.py +36 -37
- prefect/client/cloud.py +26 -19
- prefect/client/collections.py +2 -2
- prefect/client/orchestration.py +342 -273
- prefect/client/schemas/__init__.py +24 -0
- prefect/client/schemas/actions.py +123 -116
- prefect/client/schemas/objects.py +110 -81
- prefect/client/schemas/responses.py +18 -18
- prefect/client/schemas/schedules.py +136 -93
- prefect/client/subscriptions.py +28 -14
- prefect/client/utilities.py +32 -36
- prefect/concurrency/asyncio.py +6 -9
- prefect/concurrency/sync.py +35 -5
- prefect/context.py +39 -31
- prefect/deployments/flow_runs.py +3 -5
- prefect/docker/__init__.py +1 -1
- prefect/events/schemas/events.py +25 -20
- prefect/events/utilities.py +1 -2
- prefect/filesystems.py +3 -3
- prefect/flow_engine.py +61 -21
- prefect/flow_runs.py +3 -3
- prefect/flows.py +214 -170
- prefect/logging/configuration.py +1 -1
- prefect/logging/highlighters.py +1 -2
- prefect/logging/loggers.py +30 -20
- prefect/main.py +17 -24
- prefect/runner/runner.py +43 -21
- prefect/runner/server.py +30 -32
- prefect/runner/submit.py +3 -6
- prefect/runner/utils.py +6 -6
- prefect/runtime/flow_run.py +7 -0
- prefect/settings/constants.py +2 -2
- prefect/settings/legacy.py +1 -1
- prefect/settings/models/server/events.py +10 -0
- prefect/task_engine.py +72 -19
- prefect/task_runners.py +2 -2
- prefect/tasks.py +46 -33
- prefect/telemetry/bootstrap.py +15 -2
- prefect/telemetry/run_telemetry.py +107 -0
- prefect/transactions.py +14 -14
- prefect/types/__init__.py +1 -4
- prefect/utilities/_engine.py +96 -0
- prefect/utilities/annotations.py +25 -18
- prefect/utilities/asyncutils.py +126 -140
- prefect/utilities/callables.py +87 -78
- prefect/utilities/collections.py +278 -117
- prefect/utilities/compat.py +13 -21
- prefect/utilities/context.py +6 -5
- prefect/utilities/dispatch.py +23 -12
- prefect/utilities/dockerutils.py +33 -32
- prefect/utilities/engine.py +126 -239
- prefect/utilities/filesystem.py +18 -15
- prefect/utilities/hashing.py +10 -11
- prefect/utilities/importtools.py +40 -27
- prefect/utilities/math.py +9 -5
- prefect/utilities/names.py +3 -3
- prefect/utilities/processutils.py +121 -57
- prefect/utilities/pydantic.py +41 -36
- prefect/utilities/render_swagger.py +22 -12
- prefect/utilities/schema_tools/__init__.py +2 -1
- prefect/utilities/schema_tools/hydration.py +50 -43
- prefect/utilities/schema_tools/validation.py +52 -42
- prefect/utilities/services.py +13 -12
- prefect/utilities/templating.py +45 -45
- prefect/utilities/text.py +2 -1
- prefect/utilities/timeout.py +4 -4
- prefect/utilities/urls.py +9 -4
- prefect/utilities/visualization.py +46 -24
- prefect/variables.py +9 -8
- prefect/workers/base.py +15 -8
- {prefect_client-3.1.5.dist-info → prefect_client-3.1.6.dist-info}/METADATA +4 -2
- {prefect_client-3.1.5.dist-info → prefect_client-3.1.6.dist-info}/RECORD +93 -91
- {prefect_client-3.1.5.dist-info → prefect_client-3.1.6.dist-info}/LICENSE +0 -0
- {prefect_client-3.1.5.dist-info → prefect_client-3.1.6.dist-info}/WHEEL +0 -0
- {prefect_client-3.1.5.dist-info → prefect_client-3.1.6.dist-info}/top_level.txt +0 -0
@@ -4,28 +4,35 @@ import signal
|
|
4
4
|
import subprocess
|
5
5
|
import sys
|
6
6
|
import threading
|
7
|
+
from collections.abc import AsyncGenerator, Mapping
|
7
8
|
from contextlib import asynccontextmanager
|
8
9
|
from dataclasses import dataclass
|
9
10
|
from functools import partial
|
11
|
+
from types import FrameType
|
10
12
|
from typing import (
|
11
13
|
IO,
|
14
|
+
TYPE_CHECKING,
|
12
15
|
Any,
|
16
|
+
AnyStr,
|
13
17
|
Callable,
|
14
|
-
List,
|
15
|
-
Mapping,
|
16
18
|
Optional,
|
17
|
-
Sequence,
|
18
19
|
TextIO,
|
19
|
-
Tuple,
|
20
20
|
Union,
|
21
|
+
cast,
|
22
|
+
overload,
|
21
23
|
)
|
22
24
|
|
23
25
|
import anyio
|
24
26
|
import anyio.abc
|
25
27
|
from anyio.streams.text import TextReceiveStream, TextSendStream
|
28
|
+
from typing_extensions import TypeAlias, TypeVar
|
26
29
|
|
27
|
-
|
30
|
+
if TYPE_CHECKING:
|
31
|
+
from _typeshed import StrOrBytesPath
|
28
32
|
|
33
|
+
TextSink: TypeAlias = Union[anyio.AsyncFile[AnyStr], TextIO, TextSendStream]
|
34
|
+
PrintFn: TypeAlias = Callable[[str], object]
|
35
|
+
T = TypeVar("T", infer_variance=True)
|
29
36
|
|
30
37
|
if sys.platform == "win32":
|
31
38
|
from ctypes import WINFUNCTYPE, c_int, c_uint, windll
|
@@ -33,7 +40,7 @@ if sys.platform == "win32":
|
|
33
40
|
_windows_process_group_pids = set()
|
34
41
|
|
35
42
|
@WINFUNCTYPE(c_int, c_uint)
|
36
|
-
def _win32_ctrl_handler(dwCtrlType):
|
43
|
+
def _win32_ctrl_handler(dwCtrlType: object) -> int:
|
37
44
|
"""
|
38
45
|
A callback function for handling CTRL events cleanly on Windows. When called,
|
39
46
|
this function will terminate all running win32 subprocesses the current
|
@@ -125,16 +132,16 @@ if sys.platform == "win32":
|
|
125
132
|
return self._stderr
|
126
133
|
|
127
134
|
async def _open_anyio_process(
|
128
|
-
command: Union[str, bytes,
|
135
|
+
command: Union[str, bytes, list["StrOrBytesPath"]],
|
129
136
|
*,
|
130
137
|
stdin: Union[int, IO[Any], None] = None,
|
131
138
|
stdout: Union[int, IO[Any], None] = None,
|
132
139
|
stderr: Union[int, IO[Any], None] = None,
|
133
|
-
cwd:
|
134
|
-
env:
|
140
|
+
cwd: Optional["StrOrBytesPath"] = None,
|
141
|
+
env: Optional[Mapping[str, str]] = None,
|
135
142
|
start_new_session: bool = False,
|
136
|
-
**kwargs,
|
137
|
-
):
|
143
|
+
**kwargs: Any,
|
144
|
+
) -> Process:
|
138
145
|
"""
|
139
146
|
Open a subprocess and return a `Process` object.
|
140
147
|
|
@@ -179,7 +186,9 @@ if sys.platform == "win32":
|
|
179
186
|
|
180
187
|
|
181
188
|
@asynccontextmanager
|
182
|
-
async def open_process(
|
189
|
+
async def open_process(
|
190
|
+
command: list[str], **kwargs: Any
|
191
|
+
) -> AsyncGenerator[anyio.abc.Process, Any]:
|
183
192
|
"""
|
184
193
|
Like `anyio.open_process` but with:
|
185
194
|
- Support for Windows command joining
|
@@ -189,11 +198,12 @@ async def open_process(command: List[str], **kwargs):
|
|
189
198
|
# Passing a string to open_process is equivalent to shell=True which is
|
190
199
|
# generally necessary for Unix-like commands on Windows but otherwise should
|
191
200
|
# be avoided
|
192
|
-
if not
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
201
|
+
if not TYPE_CHECKING:
|
202
|
+
if not isinstance(command, list):
|
203
|
+
raise TypeError(
|
204
|
+
"The command passed to open process must be a list. You passed the command"
|
205
|
+
f"'{command}', which is type '{type(command)}'."
|
206
|
+
)
|
197
207
|
|
198
208
|
if sys.platform == "win32":
|
199
209
|
command = " ".join(command)
|
@@ -222,7 +232,7 @@ async def open_process(command: List[str], **kwargs):
|
|
222
232
|
finally:
|
223
233
|
try:
|
224
234
|
process.terminate()
|
225
|
-
if win32_process_group:
|
235
|
+
if sys.platform == "win32" and win32_process_group:
|
226
236
|
_windows_process_group_pids.remove(process.pid)
|
227
237
|
|
228
238
|
except OSError:
|
@@ -236,13 +246,58 @@ async def open_process(command: List[str], **kwargs):
|
|
236
246
|
await process.aclose()
|
237
247
|
|
238
248
|
|
249
|
+
@overload
|
250
|
+
async def run_process(
|
251
|
+
command: list[str],
|
252
|
+
*,
|
253
|
+
stream_output: Union[
|
254
|
+
bool, tuple[Optional[TextSink[str]], Optional[TextSink[str]]]
|
255
|
+
] = ...,
|
256
|
+
task_status: anyio.abc.TaskStatus[T] = ...,
|
257
|
+
task_status_handler: Callable[[anyio.abc.Process], T] = ...,
|
258
|
+
**kwargs: Any,
|
259
|
+
) -> anyio.abc.Process:
|
260
|
+
...
|
261
|
+
|
262
|
+
|
263
|
+
@overload
|
264
|
+
async def run_process(
|
265
|
+
command: list[str],
|
266
|
+
*,
|
267
|
+
stream_output: Union[
|
268
|
+
bool, tuple[Optional[TextSink[str]], Optional[TextSink[str]]]
|
269
|
+
] = ...,
|
270
|
+
task_status: Optional[anyio.abc.TaskStatus[int]] = ...,
|
271
|
+
task_status_handler: None = None,
|
272
|
+
**kwargs: Any,
|
273
|
+
) -> anyio.abc.Process:
|
274
|
+
...
|
275
|
+
|
276
|
+
|
277
|
+
@overload
|
278
|
+
async def run_process(
|
279
|
+
command: list[str],
|
280
|
+
*,
|
281
|
+
stream_output: Union[
|
282
|
+
bool, tuple[Optional[TextSink[str]], Optional[TextSink[str]]]
|
283
|
+
] = False,
|
284
|
+
task_status: Optional[anyio.abc.TaskStatus[T]] = None,
|
285
|
+
task_status_handler: Optional[Callable[[anyio.abc.Process], T]] = None,
|
286
|
+
**kwargs: Any,
|
287
|
+
) -> anyio.abc.Process:
|
288
|
+
...
|
289
|
+
|
290
|
+
|
239
291
|
async def run_process(
|
240
|
-
command:
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
292
|
+
command: list[str],
|
293
|
+
*,
|
294
|
+
stream_output: Union[
|
295
|
+
bool, tuple[Optional[TextSink[str]], Optional[TextSink[str]]]
|
296
|
+
] = False,
|
297
|
+
task_status: Optional[anyio.abc.TaskStatus[T]] = None,
|
298
|
+
task_status_handler: Optional[Callable[[anyio.abc.Process], T]] = None,
|
299
|
+
**kwargs: Any,
|
300
|
+
) -> anyio.abc.Process:
|
246
301
|
"""
|
247
302
|
Like `anyio.run_process` but with:
|
248
303
|
|
@@ -262,12 +317,10 @@ async def run_process(
|
|
262
317
|
**kwargs,
|
263
318
|
) as process:
|
264
319
|
if task_status is not None:
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
task_status.started(task_status_handler(process))
|
320
|
+
value: T = cast(T, process.pid)
|
321
|
+
if task_status_handler:
|
322
|
+
value = task_status_handler(process)
|
323
|
+
task_status.started(value)
|
271
324
|
|
272
325
|
if stream_output:
|
273
326
|
await consume_process_output(
|
@@ -280,31 +333,36 @@ async def run_process(
|
|
280
333
|
|
281
334
|
|
282
335
|
async def consume_process_output(
|
283
|
-
process,
|
284
|
-
stdout_sink: Optional[TextSink] = None,
|
285
|
-
stderr_sink: Optional[TextSink] = None,
|
286
|
-
):
|
336
|
+
process: anyio.abc.Process,
|
337
|
+
stdout_sink: Optional[TextSink[str]] = None,
|
338
|
+
stderr_sink: Optional[TextSink[str]] = None,
|
339
|
+
) -> None:
|
287
340
|
async with anyio.create_task_group() as tg:
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
341
|
+
if process.stdout is not None:
|
342
|
+
tg.start_soon(
|
343
|
+
stream_text,
|
344
|
+
TextReceiveStream(process.stdout),
|
345
|
+
stdout_sink,
|
346
|
+
)
|
347
|
+
if process.stderr is not None:
|
348
|
+
tg.start_soon(
|
349
|
+
stream_text,
|
350
|
+
TextReceiveStream(process.stderr),
|
351
|
+
stderr_sink,
|
352
|
+
)
|
298
353
|
|
299
354
|
|
300
|
-
async def stream_text(
|
355
|
+
async def stream_text(
|
356
|
+
source: TextReceiveStream, *sinks: Optional[TextSink[str]]
|
357
|
+
) -> None:
|
301
358
|
wrapped_sinks = [
|
302
359
|
(
|
303
|
-
anyio.wrap_file(sink)
|
360
|
+
anyio.wrap_file(cast(IO[str], sink))
|
304
361
|
if hasattr(sink, "write") and hasattr(sink, "flush")
|
305
362
|
else sink
|
306
363
|
)
|
307
364
|
for sink in sinks
|
365
|
+
if sink is not None
|
308
366
|
]
|
309
367
|
async for item in source:
|
310
368
|
for sink in wrapped_sinks:
|
@@ -313,30 +371,32 @@ async def stream_text(source: TextReceiveStream, *sinks: TextSink):
|
|
313
371
|
elif isinstance(sink, anyio.AsyncFile):
|
314
372
|
await sink.write(item)
|
315
373
|
await sink.flush()
|
316
|
-
elif sink is None:
|
317
|
-
pass # Consume the item but perform no action
|
318
|
-
else:
|
319
|
-
raise TypeError(f"Unsupported sink type {type(sink).__name__}")
|
320
374
|
|
321
375
|
|
322
|
-
def _register_signal(
|
376
|
+
def _register_signal(
|
377
|
+
signum: int,
|
378
|
+
handler: Optional[
|
379
|
+
Union[Callable[[int, Optional[FrameType]], Any], int, signal.Handlers]
|
380
|
+
],
|
381
|
+
) -> None:
|
323
382
|
if threading.current_thread() is threading.main_thread():
|
324
383
|
signal.signal(signum, handler)
|
325
384
|
|
326
385
|
|
327
386
|
def forward_signal_handler(
|
328
|
-
pid: int, signum: int, *signums: int, process_name: str, print_fn:
|
329
|
-
):
|
387
|
+
pid: int, signum: int, *signums: int, process_name: str, print_fn: PrintFn
|
388
|
+
) -> None:
|
330
389
|
"""Forward subsequent signum events (e.g. interrupts) to respective signums."""
|
331
390
|
current_signal, future_signals = signums[0], signums[1:]
|
332
391
|
|
333
392
|
# avoid RecursionError when setting up a direct signal forward to the same signal for the main pid
|
393
|
+
original_handler = None
|
334
394
|
avoid_infinite_recursion = signum == current_signal and pid == os.getpid()
|
335
395
|
if avoid_infinite_recursion:
|
336
396
|
# store the vanilla handler so it can be temporarily restored below
|
337
397
|
original_handler = signal.getsignal(current_signal)
|
338
398
|
|
339
|
-
def handler(*
|
399
|
+
def handler(*arg: Any) -> None:
|
340
400
|
print_fn(
|
341
401
|
f"Received {getattr(signum, 'name', signum)}. "
|
342
402
|
f"Sending {getattr(current_signal, 'name', current_signal)} to"
|
@@ -358,7 +418,9 @@ def forward_signal_handler(
|
|
358
418
|
_register_signal(signum, handler)
|
359
419
|
|
360
420
|
|
361
|
-
def setup_signal_handlers_server(
|
421
|
+
def setup_signal_handlers_server(
|
422
|
+
pid: int, process_name: str, print_fn: PrintFn
|
423
|
+
) -> None:
|
362
424
|
"""Handle interrupts of the server gracefully."""
|
363
425
|
setup_handler = partial(
|
364
426
|
forward_signal_handler, pid, process_name=process_name, print_fn=print_fn
|
@@ -375,7 +437,7 @@ def setup_signal_handlers_server(pid: int, process_name: str, print_fn: Callable
|
|
375
437
|
setup_handler(signal.SIGTERM, signal.SIGTERM, signal.SIGKILL)
|
376
438
|
|
377
439
|
|
378
|
-
def setup_signal_handlers_agent(pid: int, process_name: str, print_fn:
|
440
|
+
def setup_signal_handlers_agent(pid: int, process_name: str, print_fn: PrintFn) -> None:
|
379
441
|
"""Handle interrupts of the agent gracefully."""
|
380
442
|
setup_handler = partial(
|
381
443
|
forward_signal_handler, pid, process_name=process_name, print_fn=print_fn
|
@@ -393,7 +455,9 @@ def setup_signal_handlers_agent(pid: int, process_name: str, print_fn: Callable)
|
|
393
455
|
setup_handler(signal.SIGTERM, signal.SIGINT, signal.SIGKILL)
|
394
456
|
|
395
457
|
|
396
|
-
def setup_signal_handlers_worker(
|
458
|
+
def setup_signal_handlers_worker(
|
459
|
+
pid: int, process_name: str, print_fn: PrintFn
|
460
|
+
) -> None:
|
397
461
|
"""Handle interrupts of workers gracefully."""
|
398
462
|
setup_handler = partial(
|
399
463
|
forward_signal_handler, pid, process_name=process_name, print_fn=print_fn
|
prefect/utilities/pydantic.py
CHANGED
@@ -1,18 +1,18 @@
|
|
1
|
-
from functools import partial
|
2
1
|
from typing import (
|
3
2
|
Any,
|
4
3
|
Callable,
|
5
|
-
Dict,
|
6
4
|
Generic,
|
7
5
|
Optional,
|
8
|
-
Type,
|
9
6
|
TypeVar,
|
7
|
+
Union,
|
10
8
|
cast,
|
11
9
|
get_origin,
|
12
10
|
overload,
|
13
11
|
)
|
14
12
|
|
15
|
-
from jsonpatch import
|
13
|
+
from jsonpatch import ( # type: ignore # no typing stubs available, see https://github.com/stefankoegl/python-json-patch/issues/158
|
14
|
+
JsonPatch as JsonPatchBase,
|
15
|
+
)
|
16
16
|
from pydantic import (
|
17
17
|
BaseModel,
|
18
18
|
GetJsonSchemaHandler,
|
@@ -33,7 +33,7 @@ M = TypeVar("M", bound=BaseModel)
|
|
33
33
|
T = TypeVar("T", bound=Any)
|
34
34
|
|
35
35
|
|
36
|
-
def _reduce_model(
|
36
|
+
def _reduce_model(self: BaseModel) -> tuple[Any, ...]:
|
37
37
|
"""
|
38
38
|
Helper for serializing a cythonized model with cloudpickle.
|
39
39
|
|
@@ -43,31 +43,33 @@ def _reduce_model(model: BaseModel):
|
|
43
43
|
return (
|
44
44
|
_unreduce_model,
|
45
45
|
(
|
46
|
-
to_qualified_name(type(
|
47
|
-
|
46
|
+
to_qualified_name(type(self)),
|
47
|
+
self.model_dump_json(**getattr(self, "__reduce_kwargs__", {})),
|
48
48
|
),
|
49
49
|
)
|
50
50
|
|
51
51
|
|
52
|
-
def _unreduce_model(model_name, json):
|
52
|
+
def _unreduce_model(model_name: str, json: str) -> Any:
|
53
53
|
"""Helper for restoring model after serialization"""
|
54
54
|
model = from_qualified_name(model_name)
|
55
55
|
return model.model_validate_json(json)
|
56
56
|
|
57
57
|
|
58
58
|
@overload
|
59
|
-
def add_cloudpickle_reduction(__model_cls:
|
59
|
+
def add_cloudpickle_reduction(__model_cls: type[M]) -> type[M]:
|
60
60
|
...
|
61
61
|
|
62
62
|
|
63
63
|
@overload
|
64
64
|
def add_cloudpickle_reduction(
|
65
|
-
**kwargs: Any
|
66
|
-
) -> Callable[[
|
65
|
+
__model_cls: None = None, **kwargs: Any
|
66
|
+
) -> Callable[[type[M]], type[M]]:
|
67
67
|
...
|
68
68
|
|
69
69
|
|
70
|
-
def add_cloudpickle_reduction(
|
70
|
+
def add_cloudpickle_reduction(
|
71
|
+
__model_cls: Optional[type[M]] = None, **kwargs: Any
|
72
|
+
) -> Union[type[M], Callable[[type[M]], type[M]]]:
|
71
73
|
"""
|
72
74
|
Adds a `__reducer__` to the given class that ensures it is cloudpickle compatible.
|
73
75
|
|
@@ -85,25 +87,22 @@ def add_cloudpickle_reduction(__model_cls: Optional[Type[M]] = None, **kwargs: A
|
|
85
87
|
"""
|
86
88
|
if __model_cls:
|
87
89
|
__model_cls.__reduce__ = _reduce_model
|
88
|
-
__model_cls
|
90
|
+
setattr(__model_cls, "__reduce_kwargs__", kwargs)
|
89
91
|
return __model_cls
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
**kwargs,
|
96
|
-
),
|
97
|
-
)
|
92
|
+
|
93
|
+
def reducer_with_kwargs(__model_cls: type[M]) -> type[M]:
|
94
|
+
return add_cloudpickle_reduction(__model_cls, **kwargs)
|
95
|
+
|
96
|
+
return reducer_with_kwargs
|
98
97
|
|
99
98
|
|
100
|
-
def get_class_fields_only(model:
|
99
|
+
def get_class_fields_only(model: type[BaseModel]) -> set[str]:
|
101
100
|
"""
|
102
101
|
Gets all the field names defined on the model class but not any parent classes.
|
103
102
|
Any fields that are on the parent but redefined on the subclass are included.
|
104
103
|
"""
|
105
104
|
subclass_class_fields = set(model.__annotations__.keys())
|
106
|
-
parent_class_fields = set()
|
105
|
+
parent_class_fields: set[str] = set()
|
107
106
|
|
108
107
|
for base in model.__class__.__bases__:
|
109
108
|
if issubclass(base, BaseModel):
|
@@ -114,7 +113,7 @@ def get_class_fields_only(model: Type[BaseModel]) -> set:
|
|
114
113
|
)
|
115
114
|
|
116
115
|
|
117
|
-
def add_type_dispatch(model_cls:
|
116
|
+
def add_type_dispatch(model_cls: type[M]) -> type[M]:
|
118
117
|
"""
|
119
118
|
Extend a Pydantic model to add a 'type' field that is used as a discriminator field
|
120
119
|
to dynamically determine the subtype that when deserializing models.
|
@@ -149,7 +148,7 @@ def add_type_dispatch(model_cls: Type[M]) -> Type[M]:
|
|
149
148
|
|
150
149
|
elif not defines_dispatch_key and defines_type_field:
|
151
150
|
field_type_annotation = model_cls.model_fields["type"].annotation
|
152
|
-
if field_type_annotation != str:
|
151
|
+
if field_type_annotation != str and field_type_annotation is not None:
|
153
152
|
raise TypeError(
|
154
153
|
f"Model class {model_cls.__name__!r} defines a 'type' field with "
|
155
154
|
f"type {field_type_annotation.__name__!r} but it must be 'str'."
|
@@ -157,10 +156,10 @@ def add_type_dispatch(model_cls: Type[M]) -> Type[M]:
|
|
157
156
|
|
158
157
|
# Set the dispatch key to retrieve the value from the type field
|
159
158
|
@classmethod
|
160
|
-
def dispatch_key_from_type_field(cls):
|
159
|
+
def dispatch_key_from_type_field(cls: type[M]) -> str:
|
161
160
|
return cls.model_fields["type"].default
|
162
161
|
|
163
|
-
model_cls
|
162
|
+
setattr(model_cls, "__dispatch_key__", dispatch_key_from_type_field)
|
164
163
|
|
165
164
|
else:
|
166
165
|
raise ValueError(
|
@@ -171,7 +170,7 @@ def add_type_dispatch(model_cls: Type[M]) -> Type[M]:
|
|
171
170
|
cls_init = model_cls.__init__
|
172
171
|
cls_new = model_cls.__new__
|
173
172
|
|
174
|
-
def __init__(__pydantic_self__, **data: Any) -> None:
|
173
|
+
def __init__(__pydantic_self__: M, **data: Any) -> None:
|
175
174
|
type_string = (
|
176
175
|
get_dispatch_key(__pydantic_self__)
|
177
176
|
if type(__pydantic_self__) != model_cls
|
@@ -180,12 +179,16 @@ def add_type_dispatch(model_cls: Type[M]) -> Type[M]:
|
|
180
179
|
data.setdefault("type", type_string)
|
181
180
|
cls_init(__pydantic_self__, **data)
|
182
181
|
|
183
|
-
def __new__(cls:
|
182
|
+
def __new__(cls: type[M], **kwargs: Any) -> M:
|
184
183
|
if "type" in kwargs:
|
185
184
|
try:
|
186
185
|
subcls = lookup_type(cls, dispatch_key=kwargs["type"])
|
187
186
|
except KeyError as exc:
|
188
|
-
raise ValidationError(
|
187
|
+
raise ValidationError.from_exception_data(
|
188
|
+
title=cls.__name__,
|
189
|
+
line_errors=[{"type": str(exc), "input": kwargs["type"]}],
|
190
|
+
input_type="python",
|
191
|
+
)
|
189
192
|
return cls_new(subcls)
|
190
193
|
else:
|
191
194
|
return cls_new(cls)
|
@@ -221,7 +224,7 @@ class PartialModel(Generic[M]):
|
|
221
224
|
>>> model = partial_model.finalize(z=3.0)
|
222
225
|
"""
|
223
226
|
|
224
|
-
def __init__(self, __model_cls:
|
227
|
+
def __init__(self, __model_cls: type[M], **kwargs: Any) -> None:
|
225
228
|
self.fields = kwargs
|
226
229
|
# Set fields first to avoid issues if `fields` is also set on the `model_cls`
|
227
230
|
# in our custom `setattr` implementation.
|
@@ -236,11 +239,11 @@ class PartialModel(Generic[M]):
|
|
236
239
|
self.raise_if_not_in_model(name)
|
237
240
|
return self.model_cls(**self.fields, **kwargs)
|
238
241
|
|
239
|
-
def raise_if_already_set(self, name):
|
242
|
+
def raise_if_already_set(self, name: str) -> None:
|
240
243
|
if name in self.fields:
|
241
244
|
raise ValueError(f"Field {name!r} has already been set.")
|
242
245
|
|
243
|
-
def raise_if_not_in_model(self, name):
|
246
|
+
def raise_if_not_in_model(self, name: str) -> None:
|
244
247
|
if name not in self.model_cls.model_fields:
|
245
248
|
raise ValueError(f"Field {name!r} is not present in the model.")
|
246
249
|
|
@@ -290,7 +293,7 @@ class JsonPatch(JsonPatchBase):
|
|
290
293
|
|
291
294
|
|
292
295
|
def custom_pydantic_encoder(
|
293
|
-
type_encoders:
|
296
|
+
type_encoders: dict[Any, Callable[[type[Any]], Any]], obj: Any
|
294
297
|
) -> Any:
|
295
298
|
# Check the class type and its superclasses for a matching encoder
|
296
299
|
for base in obj.__class__.__mro__[:-1]:
|
@@ -359,8 +362,10 @@ def parse_obj_as(
|
|
359
362
|
"""
|
360
363
|
adapter = TypeAdapter(type_)
|
361
364
|
|
362
|
-
|
363
|
-
|
365
|
+
origin: Optional[Any] = get_origin(type_)
|
366
|
+
if origin is list and isinstance(data, dict):
|
367
|
+
values_dict: dict[Any, Any] = data
|
368
|
+
data = next(iter(values_dict.values()))
|
364
369
|
|
365
370
|
parser: Callable[[Any], T] = getattr(adapter, f"validate_{mode}")
|
366
371
|
|
@@ -8,10 +8,13 @@ import re
|
|
8
8
|
import string
|
9
9
|
import urllib.parse
|
10
10
|
from pathlib import Path
|
11
|
+
from typing import Any, Optional, cast
|
11
12
|
from xml.sax.saxutils import escape
|
12
13
|
|
13
14
|
import mkdocs.plugins
|
14
|
-
from mkdocs.
|
15
|
+
from mkdocs.config.defaults import MkDocsConfig
|
16
|
+
from mkdocs.structure.files import File, Files
|
17
|
+
from mkdocs.structure.pages import Page
|
15
18
|
|
16
19
|
USAGE_MSG = (
|
17
20
|
"Usage: '!!swagger <filename>!!' or '!!swagger-http <url>!!'. "
|
@@ -50,7 +53,7 @@ TOKEN = re.compile(r"!!swagger(?: (?P<path>[^\\/\s><&:]+))?!!")
|
|
50
53
|
TOKEN_HTTP = re.compile(r"!!swagger-http(?: (?P<path>https?://[^\s]+))?!!")
|
51
54
|
|
52
55
|
|
53
|
-
def swagger_lib(config) -> dict:
|
56
|
+
def swagger_lib(config: MkDocsConfig) -> dict[str, Any]:
|
54
57
|
"""
|
55
58
|
Provides the actual swagger library used
|
56
59
|
"""
|
@@ -59,11 +62,14 @@ def swagger_lib(config) -> dict:
|
|
59
62
|
"js": "https://unpkg.com/swagger-ui-dist@3/swagger-ui-bundle.js",
|
60
63
|
}
|
61
64
|
|
62
|
-
extra_javascript = config.
|
63
|
-
extra_css = config.
|
65
|
+
extra_javascript = config.extra_javascript
|
66
|
+
extra_css = cast(list[str], config.extra_css)
|
64
67
|
for lib in extra_javascript:
|
65
|
-
if
|
66
|
-
|
68
|
+
if (
|
69
|
+
os.path.basename(urllib.parse.urlparse(str(lib)).path)
|
70
|
+
== "swagger-ui-bundle.js"
|
71
|
+
):
|
72
|
+
lib_swagger["js"] = str(lib)
|
67
73
|
break
|
68
74
|
|
69
75
|
for css in extra_css:
|
@@ -73,8 +79,10 @@ def swagger_lib(config) -> dict:
|
|
73
79
|
return lib_swagger
|
74
80
|
|
75
81
|
|
76
|
-
class SwaggerPlugin(mkdocs.plugins.BasePlugin):
|
77
|
-
def on_page_markdown(
|
82
|
+
class SwaggerPlugin(mkdocs.plugins.BasePlugin[Any]):
|
83
|
+
def on_page_markdown(
|
84
|
+
self, markdown: str, /, *, page: Page, config: MkDocsConfig, files: Files
|
85
|
+
) -> Optional[str]:
|
78
86
|
is_http = False
|
79
87
|
match = TOKEN.search(markdown)
|
80
88
|
|
@@ -88,7 +96,7 @@ class SwaggerPlugin(mkdocs.plugins.BasePlugin):
|
|
88
96
|
pre_token = markdown[: match.start()]
|
89
97
|
post_token = markdown[match.end() :]
|
90
98
|
|
91
|
-
def _error(message):
|
99
|
+
def _error(message: str) -> str:
|
92
100
|
return (
|
93
101
|
pre_token
|
94
102
|
+ escape(ERROR_TEMPLATE.substitute(error=message))
|
@@ -103,8 +111,10 @@ class SwaggerPlugin(mkdocs.plugins.BasePlugin):
|
|
103
111
|
if is_http:
|
104
112
|
url = path
|
105
113
|
else:
|
114
|
+
base = page.file.abs_src_path
|
115
|
+
assert base is not None
|
106
116
|
try:
|
107
|
-
api_file = Path(
|
117
|
+
api_file = Path(base).with_name(path)
|
108
118
|
except ValueError as exc:
|
109
119
|
return _error(f"Invalid path. {exc.args[0]}")
|
110
120
|
|
@@ -114,7 +124,7 @@ class SwaggerPlugin(mkdocs.plugins.BasePlugin):
|
|
114
124
|
src_dir = api_file.parent
|
115
125
|
dest_dir = Path(page.file.abs_dest_path).parent
|
116
126
|
|
117
|
-
new_file = File(api_file.name, src_dir, dest_dir, False)
|
127
|
+
new_file = File(api_file.name, str(src_dir), str(dest_dir), False)
|
118
128
|
files.append(new_file)
|
119
129
|
url = Path(new_file.abs_dest_path).name
|
120
130
|
|
@@ -129,4 +139,4 @@ class SwaggerPlugin(mkdocs.plugins.BasePlugin):
|
|
129
139
|
)
|
130
140
|
|
131
141
|
# If multiple swaggers exist.
|
132
|
-
return self.on_page_markdown(markdown, page, config, files)
|
142
|
+
return self.on_page_markdown(markdown, page=page, config=config, files=files)
|
@@ -2,8 +2,8 @@ from .hydration import HydrationContext, HydrationError, hydrate
|
|
2
2
|
from .validation import (
|
3
3
|
CircularSchemaRefError,
|
4
4
|
ValidationError,
|
5
|
-
validate,
|
6
5
|
is_valid_schema,
|
6
|
+
validate,
|
7
7
|
)
|
8
8
|
|
9
9
|
__all__ = [
|
@@ -12,5 +12,6 @@ __all__ = [
|
|
12
12
|
"HydrationError",
|
13
13
|
"ValidationError",
|
14
14
|
"hydrate",
|
15
|
+
"is_valid_schema",
|
15
16
|
"validate",
|
16
17
|
]
|