prefect-client 2.19.3__py3-none-any.whl → 3.0.0rc1__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 (239) hide show
  1. prefect/__init__.py +8 -56
  2. prefect/_internal/compatibility/deprecated.py +6 -115
  3. prefect/_internal/compatibility/experimental.py +4 -79
  4. prefect/_internal/concurrency/api.py +0 -34
  5. prefect/_internal/concurrency/calls.py +0 -6
  6. prefect/_internal/concurrency/cancellation.py +0 -3
  7. prefect/_internal/concurrency/event_loop.py +0 -20
  8. prefect/_internal/concurrency/inspection.py +3 -3
  9. prefect/_internal/concurrency/threads.py +35 -0
  10. prefect/_internal/concurrency/waiters.py +0 -28
  11. prefect/_internal/pydantic/__init__.py +0 -45
  12. prefect/_internal/pydantic/v1_schema.py +21 -22
  13. prefect/_internal/pydantic/v2_schema.py +0 -2
  14. prefect/_internal/pydantic/v2_validated_func.py +18 -23
  15. prefect/_internal/schemas/bases.py +44 -177
  16. prefect/_internal/schemas/fields.py +1 -43
  17. prefect/_internal/schemas/validators.py +60 -158
  18. prefect/artifacts.py +161 -14
  19. prefect/automations.py +39 -4
  20. prefect/blocks/abstract.py +1 -1
  21. prefect/blocks/core.py +268 -148
  22. prefect/blocks/fields.py +2 -57
  23. prefect/blocks/kubernetes.py +8 -12
  24. prefect/blocks/notifications.py +40 -20
  25. prefect/blocks/system.py +22 -11
  26. prefect/blocks/webhook.py +2 -9
  27. prefect/client/base.py +4 -4
  28. prefect/client/cloud.py +8 -13
  29. prefect/client/orchestration.py +347 -341
  30. prefect/client/schemas/actions.py +92 -86
  31. prefect/client/schemas/filters.py +20 -40
  32. prefect/client/schemas/objects.py +147 -145
  33. prefect/client/schemas/responses.py +16 -24
  34. prefect/client/schemas/schedules.py +47 -35
  35. prefect/client/subscriptions.py +2 -2
  36. prefect/client/utilities.py +5 -2
  37. prefect/concurrency/asyncio.py +3 -1
  38. prefect/concurrency/events.py +1 -1
  39. prefect/concurrency/services.py +6 -3
  40. prefect/context.py +195 -27
  41. prefect/deployments/__init__.py +5 -6
  42. prefect/deployments/base.py +7 -5
  43. prefect/deployments/flow_runs.py +185 -0
  44. prefect/deployments/runner.py +50 -45
  45. prefect/deployments/schedules.py +28 -23
  46. prefect/deployments/steps/__init__.py +0 -1
  47. prefect/deployments/steps/core.py +1 -0
  48. prefect/deployments/steps/pull.py +7 -21
  49. prefect/engine.py +12 -2422
  50. prefect/events/actions.py +17 -23
  51. prefect/events/cli/automations.py +19 -6
  52. prefect/events/clients.py +14 -37
  53. prefect/events/filters.py +14 -18
  54. prefect/events/related.py +2 -2
  55. prefect/events/schemas/__init__.py +0 -5
  56. prefect/events/schemas/automations.py +55 -46
  57. prefect/events/schemas/deployment_triggers.py +7 -197
  58. prefect/events/schemas/events.py +34 -65
  59. prefect/events/schemas/labelling.py +10 -14
  60. prefect/events/utilities.py +2 -3
  61. prefect/events/worker.py +2 -3
  62. prefect/filesystems.py +6 -517
  63. prefect/{new_flow_engine.py → flow_engine.py} +313 -72
  64. prefect/flow_runs.py +377 -5
  65. prefect/flows.py +248 -165
  66. prefect/futures.py +186 -345
  67. prefect/infrastructure/__init__.py +0 -27
  68. prefect/infrastructure/provisioners/__init__.py +5 -3
  69. prefect/infrastructure/provisioners/cloud_run.py +11 -6
  70. prefect/infrastructure/provisioners/container_instance.py +11 -7
  71. prefect/infrastructure/provisioners/ecs.py +6 -4
  72. prefect/infrastructure/provisioners/modal.py +8 -5
  73. prefect/input/actions.py +2 -4
  74. prefect/input/run_input.py +5 -7
  75. prefect/logging/formatters.py +0 -2
  76. prefect/logging/handlers.py +3 -11
  77. prefect/logging/loggers.py +2 -2
  78. prefect/manifests.py +2 -1
  79. prefect/records/__init__.py +1 -0
  80. prefect/records/result_store.py +42 -0
  81. prefect/records/store.py +9 -0
  82. prefect/results.py +43 -39
  83. prefect/runner/runner.py +9 -9
  84. prefect/runner/server.py +6 -10
  85. prefect/runner/storage.py +3 -8
  86. prefect/runner/submit.py +2 -2
  87. prefect/runner/utils.py +2 -2
  88. prefect/serializers.py +24 -35
  89. prefect/server/api/collections_data/views/aggregate-worker-metadata.json +5 -14
  90. prefect/settings.py +70 -133
  91. prefect/states.py +17 -47
  92. prefect/task_engine.py +697 -58
  93. prefect/task_runners.py +269 -301
  94. prefect/task_server.py +53 -34
  95. prefect/tasks.py +327 -337
  96. prefect/transactions.py +220 -0
  97. prefect/types/__init__.py +61 -82
  98. prefect/utilities/asyncutils.py +195 -136
  99. prefect/utilities/callables.py +121 -41
  100. prefect/utilities/collections.py +23 -38
  101. prefect/utilities/dispatch.py +11 -3
  102. prefect/utilities/dockerutils.py +4 -0
  103. prefect/utilities/engine.py +140 -20
  104. prefect/utilities/importtools.py +26 -27
  105. prefect/utilities/pydantic.py +128 -38
  106. prefect/utilities/schema_tools/hydration.py +5 -1
  107. prefect/utilities/templating.py +12 -2
  108. prefect/variables.py +78 -61
  109. prefect/workers/__init__.py +0 -1
  110. prefect/workers/base.py +15 -17
  111. prefect/workers/process.py +3 -8
  112. prefect/workers/server.py +2 -2
  113. {prefect_client-2.19.3.dist-info → prefect_client-3.0.0rc1.dist-info}/METADATA +22 -21
  114. prefect_client-3.0.0rc1.dist-info/RECORD +176 -0
  115. prefect/_internal/pydantic/_base_model.py +0 -51
  116. prefect/_internal/pydantic/_compat.py +0 -82
  117. prefect/_internal/pydantic/_flags.py +0 -20
  118. prefect/_internal/pydantic/_types.py +0 -8
  119. prefect/_internal/pydantic/utilities/__init__.py +0 -0
  120. prefect/_internal/pydantic/utilities/config_dict.py +0 -72
  121. prefect/_internal/pydantic/utilities/field_validator.py +0 -150
  122. prefect/_internal/pydantic/utilities/model_construct.py +0 -56
  123. prefect/_internal/pydantic/utilities/model_copy.py +0 -55
  124. prefect/_internal/pydantic/utilities/model_dump.py +0 -136
  125. prefect/_internal/pydantic/utilities/model_dump_json.py +0 -112
  126. prefect/_internal/pydantic/utilities/model_fields.py +0 -50
  127. prefect/_internal/pydantic/utilities/model_fields_set.py +0 -29
  128. prefect/_internal/pydantic/utilities/model_json_schema.py +0 -82
  129. prefect/_internal/pydantic/utilities/model_rebuild.py +0 -80
  130. prefect/_internal/pydantic/utilities/model_validate.py +0 -75
  131. prefect/_internal/pydantic/utilities/model_validate_json.py +0 -68
  132. prefect/_internal/pydantic/utilities/model_validator.py +0 -87
  133. prefect/_internal/pydantic/utilities/type_adapter.py +0 -71
  134. prefect/_vendor/__init__.py +0 -0
  135. prefect/_vendor/fastapi/__init__.py +0 -25
  136. prefect/_vendor/fastapi/applications.py +0 -946
  137. prefect/_vendor/fastapi/background.py +0 -3
  138. prefect/_vendor/fastapi/concurrency.py +0 -44
  139. prefect/_vendor/fastapi/datastructures.py +0 -58
  140. prefect/_vendor/fastapi/dependencies/__init__.py +0 -0
  141. prefect/_vendor/fastapi/dependencies/models.py +0 -64
  142. prefect/_vendor/fastapi/dependencies/utils.py +0 -877
  143. prefect/_vendor/fastapi/encoders.py +0 -177
  144. prefect/_vendor/fastapi/exception_handlers.py +0 -40
  145. prefect/_vendor/fastapi/exceptions.py +0 -46
  146. prefect/_vendor/fastapi/logger.py +0 -3
  147. prefect/_vendor/fastapi/middleware/__init__.py +0 -1
  148. prefect/_vendor/fastapi/middleware/asyncexitstack.py +0 -25
  149. prefect/_vendor/fastapi/middleware/cors.py +0 -3
  150. prefect/_vendor/fastapi/middleware/gzip.py +0 -3
  151. prefect/_vendor/fastapi/middleware/httpsredirect.py +0 -3
  152. prefect/_vendor/fastapi/middleware/trustedhost.py +0 -3
  153. prefect/_vendor/fastapi/middleware/wsgi.py +0 -3
  154. prefect/_vendor/fastapi/openapi/__init__.py +0 -0
  155. prefect/_vendor/fastapi/openapi/constants.py +0 -2
  156. prefect/_vendor/fastapi/openapi/docs.py +0 -203
  157. prefect/_vendor/fastapi/openapi/models.py +0 -480
  158. prefect/_vendor/fastapi/openapi/utils.py +0 -485
  159. prefect/_vendor/fastapi/param_functions.py +0 -340
  160. prefect/_vendor/fastapi/params.py +0 -453
  161. prefect/_vendor/fastapi/requests.py +0 -4
  162. prefect/_vendor/fastapi/responses.py +0 -40
  163. prefect/_vendor/fastapi/routing.py +0 -1331
  164. prefect/_vendor/fastapi/security/__init__.py +0 -15
  165. prefect/_vendor/fastapi/security/api_key.py +0 -98
  166. prefect/_vendor/fastapi/security/base.py +0 -6
  167. prefect/_vendor/fastapi/security/http.py +0 -172
  168. prefect/_vendor/fastapi/security/oauth2.py +0 -227
  169. prefect/_vendor/fastapi/security/open_id_connect_url.py +0 -34
  170. prefect/_vendor/fastapi/security/utils.py +0 -10
  171. prefect/_vendor/fastapi/staticfiles.py +0 -1
  172. prefect/_vendor/fastapi/templating.py +0 -3
  173. prefect/_vendor/fastapi/testclient.py +0 -1
  174. prefect/_vendor/fastapi/types.py +0 -3
  175. prefect/_vendor/fastapi/utils.py +0 -235
  176. prefect/_vendor/fastapi/websockets.py +0 -7
  177. prefect/_vendor/starlette/__init__.py +0 -1
  178. prefect/_vendor/starlette/_compat.py +0 -28
  179. prefect/_vendor/starlette/_exception_handler.py +0 -80
  180. prefect/_vendor/starlette/_utils.py +0 -88
  181. prefect/_vendor/starlette/applications.py +0 -261
  182. prefect/_vendor/starlette/authentication.py +0 -159
  183. prefect/_vendor/starlette/background.py +0 -43
  184. prefect/_vendor/starlette/concurrency.py +0 -59
  185. prefect/_vendor/starlette/config.py +0 -151
  186. prefect/_vendor/starlette/convertors.py +0 -87
  187. prefect/_vendor/starlette/datastructures.py +0 -707
  188. prefect/_vendor/starlette/endpoints.py +0 -130
  189. prefect/_vendor/starlette/exceptions.py +0 -60
  190. prefect/_vendor/starlette/formparsers.py +0 -276
  191. prefect/_vendor/starlette/middleware/__init__.py +0 -17
  192. prefect/_vendor/starlette/middleware/authentication.py +0 -52
  193. prefect/_vendor/starlette/middleware/base.py +0 -220
  194. prefect/_vendor/starlette/middleware/cors.py +0 -176
  195. prefect/_vendor/starlette/middleware/errors.py +0 -265
  196. prefect/_vendor/starlette/middleware/exceptions.py +0 -74
  197. prefect/_vendor/starlette/middleware/gzip.py +0 -113
  198. prefect/_vendor/starlette/middleware/httpsredirect.py +0 -19
  199. prefect/_vendor/starlette/middleware/sessions.py +0 -82
  200. prefect/_vendor/starlette/middleware/trustedhost.py +0 -64
  201. prefect/_vendor/starlette/middleware/wsgi.py +0 -147
  202. prefect/_vendor/starlette/requests.py +0 -328
  203. prefect/_vendor/starlette/responses.py +0 -347
  204. prefect/_vendor/starlette/routing.py +0 -933
  205. prefect/_vendor/starlette/schemas.py +0 -154
  206. prefect/_vendor/starlette/staticfiles.py +0 -248
  207. prefect/_vendor/starlette/status.py +0 -199
  208. prefect/_vendor/starlette/templating.py +0 -231
  209. prefect/_vendor/starlette/testclient.py +0 -804
  210. prefect/_vendor/starlette/types.py +0 -30
  211. prefect/_vendor/starlette/websockets.py +0 -193
  212. prefect/agent.py +0 -698
  213. prefect/deployments/deployments.py +0 -1042
  214. prefect/deprecated/__init__.py +0 -0
  215. prefect/deprecated/data_documents.py +0 -350
  216. prefect/deprecated/packaging/__init__.py +0 -12
  217. prefect/deprecated/packaging/base.py +0 -96
  218. prefect/deprecated/packaging/docker.py +0 -146
  219. prefect/deprecated/packaging/file.py +0 -92
  220. prefect/deprecated/packaging/orion.py +0 -80
  221. prefect/deprecated/packaging/serializers.py +0 -171
  222. prefect/events/instrument.py +0 -135
  223. prefect/infrastructure/base.py +0 -323
  224. prefect/infrastructure/container.py +0 -818
  225. prefect/infrastructure/kubernetes.py +0 -920
  226. prefect/infrastructure/process.py +0 -289
  227. prefect/new_task_engine.py +0 -423
  228. prefect/pydantic/__init__.py +0 -76
  229. prefect/pydantic/main.py +0 -39
  230. prefect/software/__init__.py +0 -2
  231. prefect/software/base.py +0 -50
  232. prefect/software/conda.py +0 -199
  233. prefect/software/pip.py +0 -122
  234. prefect/software/python.py +0 -52
  235. prefect/workers/block.py +0 -218
  236. prefect_client-2.19.3.dist-info/RECORD +0 -292
  237. {prefect_client-2.19.3.dist-info → prefect_client-3.0.0rc1.dist-info}/LICENSE +0 -0
  238. {prefect_client-2.19.3.dist-info → prefect_client-3.0.0rc1.dist-info}/WHEEL +0 -0
  239. {prefect_client-2.19.3.dist-info → prefect_client-3.0.0rc1.dist-info}/top_level.txt +0 -0
@@ -1,130 +0,0 @@
1
- import json
2
- import typing
3
-
4
- from prefect._vendor.starlette import status
5
- from prefect._vendor.starlette._utils import is_async_callable
6
- from prefect._vendor.starlette.concurrency import run_in_threadpool
7
- from prefect._vendor.starlette.exceptions import HTTPException
8
- from prefect._vendor.starlette.requests import Request
9
- from prefect._vendor.starlette.responses import PlainTextResponse, Response
10
- from prefect._vendor.starlette.types import Message, Receive, Scope, Send
11
- from prefect._vendor.starlette.websockets import WebSocket
12
-
13
-
14
- class HTTPEndpoint:
15
- def __init__(self, scope: Scope, receive: Receive, send: Send) -> None:
16
- assert scope["type"] == "http"
17
- self.scope = scope
18
- self.receive = receive
19
- self.send = send
20
- self._allowed_methods = [
21
- method
22
- for method in ("GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
23
- if getattr(self, method.lower(), None) is not None
24
- ]
25
-
26
- def __await__(self) -> typing.Generator[typing.Any, None, None]:
27
- return self.dispatch().__await__()
28
-
29
- async def dispatch(self) -> None:
30
- request = Request(self.scope, receive=self.receive)
31
- handler_name = (
32
- "get"
33
- if request.method == "HEAD" and not hasattr(self, "head")
34
- else request.method.lower()
35
- )
36
-
37
- handler: typing.Callable[[Request], typing.Any] = getattr(
38
- self, handler_name, self.method_not_allowed
39
- )
40
- is_async = is_async_callable(handler)
41
- if is_async:
42
- response = await handler(request)
43
- else:
44
- response = await run_in_threadpool(handler, request)
45
- await response(self.scope, self.receive, self.send)
46
-
47
- async def method_not_allowed(self, request: Request) -> Response:
48
- # If we're running inside a starlette application then raise an
49
- # exception, so that the configurable exception handler can deal with
50
- # returning the response. For plain ASGI apps, just return the response.
51
- headers = {"Allow": ", ".join(self._allowed_methods)}
52
- if "app" in self.scope:
53
- raise HTTPException(status_code=405, headers=headers)
54
- return PlainTextResponse("Method Not Allowed", status_code=405, headers=headers)
55
-
56
-
57
- class WebSocketEndpoint:
58
- encoding: typing.Optional[str] = None # May be "text", "bytes", or "json".
59
-
60
- def __init__(self, scope: Scope, receive: Receive, send: Send) -> None:
61
- assert scope["type"] == "websocket"
62
- self.scope = scope
63
- self.receive = receive
64
- self.send = send
65
-
66
- def __await__(self) -> typing.Generator[typing.Any, None, None]:
67
- return self.dispatch().__await__()
68
-
69
- async def dispatch(self) -> None:
70
- websocket = WebSocket(self.scope, receive=self.receive, send=self.send)
71
- await self.on_connect(websocket)
72
-
73
- close_code = status.WS_1000_NORMAL_CLOSURE
74
-
75
- try:
76
- while True:
77
- message = await websocket.receive()
78
- if message["type"] == "websocket.receive":
79
- data = await self.decode(websocket, message)
80
- await self.on_receive(websocket, data)
81
- elif message["type"] == "websocket.disconnect":
82
- close_code = int(
83
- message.get("code") or status.WS_1000_NORMAL_CLOSURE
84
- )
85
- break
86
- except Exception as exc:
87
- close_code = status.WS_1011_INTERNAL_ERROR
88
- raise exc
89
- finally:
90
- await self.on_disconnect(websocket, close_code)
91
-
92
- async def decode(self, websocket: WebSocket, message: Message) -> typing.Any:
93
- if self.encoding == "text":
94
- if "text" not in message:
95
- await websocket.close(code=status.WS_1003_UNSUPPORTED_DATA)
96
- raise RuntimeError("Expected text websocket messages, but got bytes")
97
- return message["text"]
98
-
99
- elif self.encoding == "bytes":
100
- if "bytes" not in message:
101
- await websocket.close(code=status.WS_1003_UNSUPPORTED_DATA)
102
- raise RuntimeError("Expected bytes websocket messages, but got text")
103
- return message["bytes"]
104
-
105
- elif self.encoding == "json":
106
- if message.get("text") is not None:
107
- text = message["text"]
108
- else:
109
- text = message["bytes"].decode("utf-8")
110
-
111
- try:
112
- return json.loads(text)
113
- except json.decoder.JSONDecodeError:
114
- await websocket.close(code=status.WS_1003_UNSUPPORTED_DATA)
115
- raise RuntimeError("Malformed JSON data received.")
116
-
117
- assert (
118
- self.encoding is None
119
- ), f"Unsupported 'encoding' attribute {self.encoding}"
120
- return message["text"] if message.get("text") else message["bytes"]
121
-
122
- async def on_connect(self, websocket: WebSocket) -> None:
123
- """Override to handle an incoming websocket connection"""
124
- await websocket.accept()
125
-
126
- async def on_receive(self, websocket: WebSocket, data: typing.Any) -> None:
127
- """Override to handle an incoming websocket message"""
128
-
129
- async def on_disconnect(self, websocket: WebSocket, close_code: int) -> None:
130
- """Override to handle a disconnecting websocket"""
@@ -1,60 +0,0 @@
1
- import http
2
- import typing
3
- import warnings
4
-
5
- __all__ = ("HTTPException", "WebSocketException")
6
-
7
-
8
- class HTTPException(Exception):
9
- def __init__(
10
- self,
11
- status_code: int,
12
- detail: typing.Optional[str] = None,
13
- headers: typing.Optional[typing.Dict[str, str]] = None,
14
- ) -> None:
15
- if detail is None:
16
- detail = http.HTTPStatus(status_code).phrase
17
- self.status_code = status_code
18
- self.detail = detail
19
- self.headers = headers
20
-
21
- def __str__(self) -> str:
22
- return f"{self.status_code}: {self.detail}"
23
-
24
- def __repr__(self) -> str:
25
- class_name = self.__class__.__name__
26
- return f"{class_name}(status_code={self.status_code!r}, detail={self.detail!r})"
27
-
28
-
29
- class WebSocketException(Exception):
30
- def __init__(self, code: int, reason: typing.Optional[str] = None) -> None:
31
- self.code = code
32
- self.reason = reason or ""
33
-
34
- def __str__(self) -> str:
35
- return f"{self.code}: {self.reason}"
36
-
37
- def __repr__(self) -> str:
38
- class_name = self.__class__.__name__
39
- return f"{class_name}(code={self.code!r}, reason={self.reason!r})"
40
-
41
-
42
- __deprecated__ = "ExceptionMiddleware"
43
-
44
-
45
- def __getattr__(name: str) -> typing.Any: # pragma: no cover
46
- if name == __deprecated__:
47
- from prefect._vendor.starlette.middleware.exceptions import ExceptionMiddleware
48
-
49
- warnings.warn(
50
- f"{__deprecated__} is deprecated on `starlette.exceptions`. "
51
- f"Import it from `starlette.middleware.exceptions` instead.",
52
- category=DeprecationWarning,
53
- stacklevel=3,
54
- )
55
- return ExceptionMiddleware
56
- raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
57
-
58
-
59
- def __dir__() -> typing.List[str]:
60
- return sorted(list(__all__) + [__deprecated__]) # pragma: no cover
@@ -1,276 +0,0 @@
1
- import typing
2
- from dataclasses import dataclass, field
3
- from enum import Enum
4
- from tempfile import SpooledTemporaryFile
5
- from urllib.parse import unquote_plus
6
-
7
- from prefect._vendor.starlette.datastructures import FormData, Headers, UploadFile
8
-
9
- try:
10
- import multipart
11
- from multipart.multipart import parse_options_header
12
- except ModuleNotFoundError: # pragma: nocover
13
- parse_options_header = None
14
- multipart = None
15
-
16
-
17
- class FormMessage(Enum):
18
- FIELD_START = 1
19
- FIELD_NAME = 2
20
- FIELD_DATA = 3
21
- FIELD_END = 4
22
- END = 5
23
-
24
-
25
- @dataclass
26
- class MultipartPart:
27
- content_disposition: typing.Optional[bytes] = None
28
- field_name: str = ""
29
- data: bytes = b""
30
- file: typing.Optional[UploadFile] = None
31
- item_headers: typing.List[typing.Tuple[bytes, bytes]] = field(default_factory=list)
32
-
33
-
34
- def _user_safe_decode(src: bytes, codec: str) -> str:
35
- try:
36
- return src.decode(codec)
37
- except (UnicodeDecodeError, LookupError):
38
- return src.decode("latin-1")
39
-
40
-
41
- class MultiPartException(Exception):
42
- def __init__(self, message: str) -> None:
43
- self.message = message
44
-
45
-
46
- class FormParser:
47
- def __init__(
48
- self, headers: Headers, stream: typing.AsyncGenerator[bytes, None]
49
- ) -> None:
50
- assert (
51
- multipart is not None
52
- ), "The `python-multipart` library must be installed to use form parsing."
53
- self.headers = headers
54
- self.stream = stream
55
- self.messages: typing.List[typing.Tuple[FormMessage, bytes]] = []
56
-
57
- def on_field_start(self) -> None:
58
- message = (FormMessage.FIELD_START, b"")
59
- self.messages.append(message)
60
-
61
- def on_field_name(self, data: bytes, start: int, end: int) -> None:
62
- message = (FormMessage.FIELD_NAME, data[start:end])
63
- self.messages.append(message)
64
-
65
- def on_field_data(self, data: bytes, start: int, end: int) -> None:
66
- message = (FormMessage.FIELD_DATA, data[start:end])
67
- self.messages.append(message)
68
-
69
- def on_field_end(self) -> None:
70
- message = (FormMessage.FIELD_END, b"")
71
- self.messages.append(message)
72
-
73
- def on_end(self) -> None:
74
- message = (FormMessage.END, b"")
75
- self.messages.append(message)
76
-
77
- async def parse(self) -> FormData:
78
- # Callbacks dictionary.
79
- callbacks = {
80
- "on_field_start": self.on_field_start,
81
- "on_field_name": self.on_field_name,
82
- "on_field_data": self.on_field_data,
83
- "on_field_end": self.on_field_end,
84
- "on_end": self.on_end,
85
- }
86
-
87
- # Create the parser.
88
- parser = multipart.QuerystringParser(callbacks)
89
- field_name = b""
90
- field_value = b""
91
-
92
- items: typing.List[typing.Tuple[str, typing.Union[str, UploadFile]]] = []
93
-
94
- # Feed the parser with data from the request.
95
- async for chunk in self.stream:
96
- if chunk:
97
- parser.write(chunk)
98
- else:
99
- parser.finalize()
100
- messages = list(self.messages)
101
- self.messages.clear()
102
- for message_type, message_bytes in messages:
103
- if message_type == FormMessage.FIELD_START:
104
- field_name = b""
105
- field_value = b""
106
- elif message_type == FormMessage.FIELD_NAME:
107
- field_name += message_bytes
108
- elif message_type == FormMessage.FIELD_DATA:
109
- field_value += message_bytes
110
- elif message_type == FormMessage.FIELD_END:
111
- name = unquote_plus(field_name.decode("latin-1"))
112
- value = unquote_plus(field_value.decode("latin-1"))
113
- items.append((name, value))
114
-
115
- return FormData(items)
116
-
117
-
118
- class MultiPartParser:
119
- max_file_size = 1024 * 1024
120
-
121
- def __init__(
122
- self,
123
- headers: Headers,
124
- stream: typing.AsyncGenerator[bytes, None],
125
- *,
126
- max_files: typing.Union[int, float] = 1000,
127
- max_fields: typing.Union[int, float] = 1000,
128
- ) -> None:
129
- assert (
130
- multipart is not None
131
- ), "The `python-multipart` library must be installed to use form parsing."
132
- self.headers = headers
133
- self.stream = stream
134
- self.max_files = max_files
135
- self.max_fields = max_fields
136
- self.items: typing.List[typing.Tuple[str, typing.Union[str, UploadFile]]] = []
137
- self._current_files = 0
138
- self._current_fields = 0
139
- self._current_partial_header_name: bytes = b""
140
- self._current_partial_header_value: bytes = b""
141
- self._current_part = MultipartPart()
142
- self._charset = ""
143
- self._file_parts_to_write: typing.List[typing.Tuple[MultipartPart, bytes]] = []
144
- self._file_parts_to_finish: typing.List[MultipartPart] = []
145
- self._files_to_close_on_error: typing.List[SpooledTemporaryFile[bytes]] = []
146
-
147
- def on_part_begin(self) -> None:
148
- self._current_part = MultipartPart()
149
-
150
- def on_part_data(self, data: bytes, start: int, end: int) -> None:
151
- message_bytes = data[start:end]
152
- if self._current_part.file is None:
153
- self._current_part.data += message_bytes
154
- else:
155
- self._file_parts_to_write.append((self._current_part, message_bytes))
156
-
157
- def on_part_end(self) -> None:
158
- if self._current_part.file is None:
159
- self.items.append(
160
- (
161
- self._current_part.field_name,
162
- _user_safe_decode(self._current_part.data, self._charset),
163
- )
164
- )
165
- else:
166
- self._file_parts_to_finish.append(self._current_part)
167
- # The file can be added to the items right now even though it's not
168
- # finished yet, because it will be finished in the `parse()` method, before
169
- # self.items is used in the return value.
170
- self.items.append((self._current_part.field_name, self._current_part.file))
171
-
172
- def on_header_field(self, data: bytes, start: int, end: int) -> None:
173
- self._current_partial_header_name += data[start:end]
174
-
175
- def on_header_value(self, data: bytes, start: int, end: int) -> None:
176
- self._current_partial_header_value += data[start:end]
177
-
178
- def on_header_end(self) -> None:
179
- field = self._current_partial_header_name.lower()
180
- if field == b"content-disposition":
181
- self._current_part.content_disposition = self._current_partial_header_value
182
- self._current_part.item_headers.append(
183
- (field, self._current_partial_header_value)
184
- )
185
- self._current_partial_header_name = b""
186
- self._current_partial_header_value = b""
187
-
188
- def on_headers_finished(self) -> None:
189
- disposition, options = parse_options_header(
190
- self._current_part.content_disposition
191
- )
192
- try:
193
- self._current_part.field_name = _user_safe_decode(
194
- options[b"name"], self._charset
195
- )
196
- except KeyError:
197
- raise MultiPartException(
198
- 'The Content-Disposition header field "name" must be ' "provided."
199
- )
200
- if b"filename" in options:
201
- self._current_files += 1
202
- if self._current_files > self.max_files:
203
- raise MultiPartException(
204
- f"Too many files. Maximum number of files is {self.max_files}."
205
- )
206
- filename = _user_safe_decode(options[b"filename"], self._charset)
207
- tempfile = SpooledTemporaryFile(max_size=self.max_file_size)
208
- self._files_to_close_on_error.append(tempfile)
209
- self._current_part.file = UploadFile(
210
- file=tempfile, # type: ignore[arg-type]
211
- size=0,
212
- filename=filename,
213
- headers=Headers(raw=self._current_part.item_headers),
214
- )
215
- else:
216
- self._current_fields += 1
217
- if self._current_fields > self.max_fields:
218
- raise MultiPartException(
219
- f"Too many fields. Maximum number of fields is {self.max_fields}."
220
- )
221
- self._current_part.file = None
222
-
223
- def on_end(self) -> None:
224
- pass
225
-
226
- async def parse(self) -> FormData:
227
- # Parse the Content-Type header to get the multipart boundary.
228
- _, params = parse_options_header(self.headers["Content-Type"])
229
- charset = params.get(b"charset", "utf-8")
230
- if isinstance(charset, bytes):
231
- charset = charset.decode("latin-1")
232
- self._charset = charset
233
- try:
234
- boundary = params[b"boundary"]
235
- except KeyError:
236
- raise MultiPartException("Missing boundary in multipart.")
237
-
238
- # Callbacks dictionary.
239
- callbacks = {
240
- "on_part_begin": self.on_part_begin,
241
- "on_part_data": self.on_part_data,
242
- "on_part_end": self.on_part_end,
243
- "on_header_field": self.on_header_field,
244
- "on_header_value": self.on_header_value,
245
- "on_header_end": self.on_header_end,
246
- "on_headers_finished": self.on_headers_finished,
247
- "on_end": self.on_end,
248
- }
249
-
250
- # Create the parser.
251
- parser = multipart.MultipartParser(boundary, callbacks)
252
- try:
253
- # Feed the parser with data from the request.
254
- async for chunk in self.stream:
255
- parser.write(chunk)
256
- # Write file data, it needs to use await with the UploadFile methods
257
- # that call the corresponding file methods *in a threadpool*,
258
- # otherwise, if they were called directly in the callback methods above
259
- # (regular, non-async functions), that would block the event loop in
260
- # the main thread.
261
- for part, data in self._file_parts_to_write:
262
- assert part.file # for type checkers
263
- await part.file.write(data)
264
- for part in self._file_parts_to_finish:
265
- assert part.file # for type checkers
266
- await part.file.seek(0)
267
- self._file_parts_to_write.clear()
268
- self._file_parts_to_finish.clear()
269
- except MultiPartException as exc:
270
- # Close all the files if there was an error.
271
- for file in self._files_to_close_on_error:
272
- file.close()
273
- raise exc
274
-
275
- parser.finalize()
276
- return FormData(self.items)
@@ -1,17 +0,0 @@
1
- import typing
2
-
3
-
4
- class Middleware:
5
- def __init__(self, cls: type, **options: typing.Any) -> None:
6
- self.cls = cls
7
- self.options = options
8
-
9
- def __iter__(self) -> typing.Iterator[typing.Any]:
10
- as_tuple = (self.cls, self.options)
11
- return iter(as_tuple)
12
-
13
- def __repr__(self) -> str:
14
- class_name = self.__class__.__name__
15
- option_strings = [f"{key}={value!r}" for key, value in self.options.items()]
16
- args_repr = ", ".join([self.cls.__name__] + option_strings)
17
- return f"{class_name}({args_repr})"
@@ -1,52 +0,0 @@
1
- import typing
2
-
3
- from prefect._vendor.starlette.authentication import (
4
- AuthCredentials,
5
- AuthenticationBackend,
6
- AuthenticationError,
7
- UnauthenticatedUser,
8
- )
9
- from prefect._vendor.starlette.requests import HTTPConnection
10
- from prefect._vendor.starlette.responses import PlainTextResponse, Response
11
- from prefect._vendor.starlette.types import ASGIApp, Receive, Scope, Send
12
-
13
-
14
- class AuthenticationMiddleware:
15
- def __init__(
16
- self,
17
- app: ASGIApp,
18
- backend: AuthenticationBackend,
19
- on_error: typing.Optional[
20
- typing.Callable[[HTTPConnection, AuthenticationError], Response]
21
- ] = None,
22
- ) -> None:
23
- self.app = app
24
- self.backend = backend
25
- self.on_error: typing.Callable[
26
- [HTTPConnection, AuthenticationError], Response
27
- ] = on_error if on_error is not None else self.default_on_error
28
-
29
- async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
30
- if scope["type"] not in ["http", "websocket"]:
31
- await self.app(scope, receive, send)
32
- return
33
-
34
- conn = HTTPConnection(scope)
35
- try:
36
- auth_result = await self.backend.authenticate(conn)
37
- except AuthenticationError as exc:
38
- response = self.on_error(conn, exc)
39
- if scope["type"] == "websocket":
40
- await send({"type": "websocket.close", "code": 1000})
41
- else:
42
- await response(scope, receive, send)
43
- return
44
-
45
- if auth_result is None:
46
- auth_result = AuthCredentials(), UnauthenticatedUser()
47
- scope["auth"], scope["user"] = auth_result
48
- await self.app(scope, receive, send)
49
-
50
- @staticmethod
51
- def default_on_error(conn: HTTPConnection, exc: Exception) -> Response:
52
- return PlainTextResponse(str(exc), status_code=400)