prefect-client 3.1.5__py3-none-any.whl → 3.1.7__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.
Files changed (114) hide show
  1. prefect/__init__.py +3 -0
  2. prefect/_experimental/__init__.py +0 -0
  3. prefect/_experimental/lineage.py +181 -0
  4. prefect/_internal/compatibility/async_dispatch.py +38 -9
  5. prefect/_internal/compatibility/migration.py +1 -1
  6. prefect/_internal/concurrency/api.py +52 -52
  7. prefect/_internal/concurrency/calls.py +59 -35
  8. prefect/_internal/concurrency/cancellation.py +34 -18
  9. prefect/_internal/concurrency/event_loop.py +7 -6
  10. prefect/_internal/concurrency/threads.py +41 -33
  11. prefect/_internal/concurrency/waiters.py +28 -21
  12. prefect/_internal/pydantic/v1_schema.py +2 -2
  13. prefect/_internal/pydantic/v2_schema.py +10 -9
  14. prefect/_internal/pydantic/v2_validated_func.py +15 -10
  15. prefect/_internal/retries.py +15 -6
  16. prefect/_internal/schemas/bases.py +11 -8
  17. prefect/_internal/schemas/validators.py +7 -5
  18. prefect/_version.py +3 -3
  19. prefect/automations.py +53 -47
  20. prefect/blocks/abstract.py +12 -10
  21. prefect/blocks/core.py +148 -19
  22. prefect/blocks/system.py +2 -1
  23. prefect/cache_policies.py +11 -11
  24. prefect/client/__init__.py +3 -1
  25. prefect/client/base.py +36 -37
  26. prefect/client/cloud.py +26 -19
  27. prefect/client/collections.py +2 -2
  28. prefect/client/orchestration.py +430 -273
  29. prefect/client/schemas/__init__.py +24 -0
  30. prefect/client/schemas/actions.py +128 -121
  31. prefect/client/schemas/filters.py +1 -1
  32. prefect/client/schemas/objects.py +114 -85
  33. prefect/client/schemas/responses.py +19 -20
  34. prefect/client/schemas/schedules.py +136 -93
  35. prefect/client/subscriptions.py +30 -15
  36. prefect/client/utilities.py +46 -36
  37. prefect/concurrency/asyncio.py +6 -9
  38. prefect/concurrency/sync.py +35 -5
  39. prefect/context.py +40 -32
  40. prefect/deployments/flow_runs.py +6 -8
  41. prefect/deployments/runner.py +14 -14
  42. prefect/deployments/steps/core.py +3 -1
  43. prefect/deployments/steps/pull.py +60 -12
  44. prefect/docker/__init__.py +1 -1
  45. prefect/events/clients.py +55 -4
  46. prefect/events/filters.py +1 -1
  47. prefect/events/related.py +2 -1
  48. prefect/events/schemas/events.py +26 -21
  49. prefect/events/utilities.py +3 -2
  50. prefect/events/worker.py +8 -0
  51. prefect/filesystems.py +3 -3
  52. prefect/flow_engine.py +87 -87
  53. prefect/flow_runs.py +7 -5
  54. prefect/flows.py +218 -176
  55. prefect/logging/configuration.py +1 -1
  56. prefect/logging/highlighters.py +1 -2
  57. prefect/logging/loggers.py +30 -20
  58. prefect/main.py +17 -24
  59. prefect/results.py +43 -22
  60. prefect/runner/runner.py +43 -21
  61. prefect/runner/server.py +30 -32
  62. prefect/runner/storage.py +3 -3
  63. prefect/runner/submit.py +3 -6
  64. prefect/runner/utils.py +6 -6
  65. prefect/runtime/flow_run.py +7 -0
  66. prefect/serializers.py +28 -24
  67. prefect/settings/constants.py +2 -2
  68. prefect/settings/legacy.py +1 -1
  69. prefect/settings/models/experiments.py +5 -0
  70. prefect/settings/models/server/events.py +10 -0
  71. prefect/task_engine.py +87 -26
  72. prefect/task_runners.py +2 -2
  73. prefect/task_worker.py +43 -25
  74. prefect/tasks.py +148 -142
  75. prefect/telemetry/bootstrap.py +15 -2
  76. prefect/telemetry/instrumentation.py +1 -1
  77. prefect/telemetry/processors.py +10 -7
  78. prefect/telemetry/run_telemetry.py +231 -0
  79. prefect/transactions.py +14 -14
  80. prefect/types/__init__.py +5 -5
  81. prefect/utilities/_engine.py +96 -0
  82. prefect/utilities/annotations.py +25 -18
  83. prefect/utilities/asyncutils.py +126 -140
  84. prefect/utilities/callables.py +87 -78
  85. prefect/utilities/collections.py +278 -117
  86. prefect/utilities/compat.py +13 -21
  87. prefect/utilities/context.py +6 -5
  88. prefect/utilities/dispatch.py +23 -12
  89. prefect/utilities/dockerutils.py +33 -32
  90. prefect/utilities/engine.py +126 -239
  91. prefect/utilities/filesystem.py +18 -15
  92. prefect/utilities/hashing.py +10 -11
  93. prefect/utilities/importtools.py +40 -27
  94. prefect/utilities/math.py +9 -5
  95. prefect/utilities/names.py +3 -3
  96. prefect/utilities/processutils.py +121 -57
  97. prefect/utilities/pydantic.py +41 -36
  98. prefect/utilities/render_swagger.py +22 -12
  99. prefect/utilities/schema_tools/__init__.py +2 -1
  100. prefect/utilities/schema_tools/hydration.py +50 -43
  101. prefect/utilities/schema_tools/validation.py +52 -42
  102. prefect/utilities/services.py +13 -12
  103. prefect/utilities/templating.py +45 -45
  104. prefect/utilities/text.py +2 -1
  105. prefect/utilities/timeout.py +4 -4
  106. prefect/utilities/urls.py +9 -4
  107. prefect/utilities/visualization.py +46 -24
  108. prefect/variables.py +136 -27
  109. prefect/workers/base.py +15 -8
  110. {prefect_client-3.1.5.dist-info → prefect_client-3.1.7.dist-info}/METADATA +5 -2
  111. {prefect_client-3.1.5.dist-info → prefect_client-3.1.7.dist-info}/RECORD +114 -110
  112. {prefect_client-3.1.5.dist-info → prefect_client-3.1.7.dist-info}/LICENSE +0 -0
  113. {prefect_client-3.1.5.dist-info → prefect_client-3.1.7.dist-info}/WHEEL +0 -0
  114. {prefect_client-3.1.5.dist-info → prefect_client-3.1.7.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
- TextSink = Union[anyio.AsyncFile, TextIO, TextSendStream]
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, Sequence[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: Union[str, bytes, os.PathLike, None] = None,
134
- env: Union[Mapping[str, str], None] = None,
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(command: List[str], **kwargs):
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 isinstance(command, list):
193
- raise TypeError(
194
- "The command passed to open process must be a list. You passed the command"
195
- f"'{command}', which is type '{type(command)}'."
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: List[str],
241
- stream_output: Union[bool, Tuple[Optional[TextSink], Optional[TextSink]]] = False,
242
- task_status: Optional[anyio.abc.TaskStatus] = None,
243
- task_status_handler: Optional[Callable[[anyio.abc.Process], Any]] = None,
244
- **kwargs,
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
- if not task_status_handler:
266
-
267
- def task_status_handler(process):
268
- return process.pid
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
- tg.start_soon(
289
- stream_text,
290
- TextReceiveStream(process.stdout),
291
- stdout_sink,
292
- )
293
- tg.start_soon(
294
- stream_text,
295
- TextReceiveStream(process.stderr),
296
- stderr_sink,
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(source: TextReceiveStream, *sinks: TextSink):
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(signum: int, handler: Callable):
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: Callable
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(*args):
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(pid: int, process_name: str, print_fn: Callable):
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: Callable):
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(pid: int, process_name: str, print_fn: Callable):
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
@@ -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 JsonPatch as JsonPatchBase
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(model: BaseModel):
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(model)),
47
- model.model_dump_json(**getattr(model, "__reduce_kwargs__", {})),
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: Type[M]) -> Type[M]:
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[[Type[M]], Type[M]]:
65
+ __model_cls: None = None, **kwargs: Any
66
+ ) -> Callable[[type[M]], type[M]]:
67
67
  ...
68
68
 
69
69
 
70
- def add_cloudpickle_reduction(__model_cls: Optional[Type[M]] = None, **kwargs: Any):
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.__reduce_kwargs__ = kwargs
90
+ setattr(__model_cls, "__reduce_kwargs__", kwargs)
89
91
  return __model_cls
90
- else:
91
- return cast(
92
- Callable[[Type[M]], Type[M]],
93
- partial(
94
- add_cloudpickle_reduction,
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: Type[BaseModel]) -> set:
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: Type[M]) -> Type[M]:
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.__dispatch_key__ = dispatch_key_from_type_field
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: Type[M], **kwargs: Any) -> M:
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(errors=[exc], model=cls)
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: Type[M], **kwargs: Any) -> None:
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: Optional[Dict[Any, Callable[[Type[Any]], Any]]], obj: Any
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
- if get_origin(type_) is list and isinstance(data, dict):
363
- data = next(iter(data.values()))
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.structure.files import File
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.get("extra_javascript", [])
63
- extra_css = config.get("extra_css", [])
65
+ extra_javascript = config.extra_javascript
66
+ extra_css = cast(list[str], config.extra_css)
64
67
  for lib in extra_javascript:
65
- if os.path.basename(urllib.parse.urlparse(lib).path) == "swagger-ui-bundle.js":
66
- lib_swagger["js"] = lib
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(self, markdown, page, config, files):
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(page.file.abs_src_path).with_name(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
  ]