prefect-client 2.19.4__py3-none-any.whl → 3.0.0rc2__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 (242) 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/redis.py +168 -0
  26. prefect/blocks/system.py +22 -11
  27. prefect/blocks/webhook.py +2 -9
  28. prefect/client/base.py +4 -4
  29. prefect/client/cloud.py +8 -13
  30. prefect/client/orchestration.py +362 -340
  31. prefect/client/schemas/actions.py +92 -86
  32. prefect/client/schemas/filters.py +20 -40
  33. prefect/client/schemas/objects.py +158 -152
  34. prefect/client/schemas/responses.py +16 -24
  35. prefect/client/schemas/schedules.py +47 -35
  36. prefect/client/subscriptions.py +2 -2
  37. prefect/client/utilities.py +5 -2
  38. prefect/concurrency/asyncio.py +4 -2
  39. prefect/concurrency/events.py +1 -1
  40. prefect/concurrency/services.py +7 -4
  41. prefect/context.py +195 -27
  42. prefect/deployments/__init__.py +5 -6
  43. prefect/deployments/base.py +7 -5
  44. prefect/deployments/flow_runs.py +185 -0
  45. prefect/deployments/runner.py +50 -45
  46. prefect/deployments/schedules.py +28 -23
  47. prefect/deployments/steps/__init__.py +0 -1
  48. prefect/deployments/steps/core.py +1 -0
  49. prefect/deployments/steps/pull.py +7 -21
  50. prefect/engine.py +12 -2422
  51. prefect/events/actions.py +17 -23
  52. prefect/events/cli/automations.py +19 -6
  53. prefect/events/clients.py +14 -37
  54. prefect/events/filters.py +14 -18
  55. prefect/events/related.py +2 -2
  56. prefect/events/schemas/__init__.py +0 -5
  57. prefect/events/schemas/automations.py +55 -46
  58. prefect/events/schemas/deployment_triggers.py +7 -197
  59. prefect/events/schemas/events.py +36 -65
  60. prefect/events/schemas/labelling.py +10 -14
  61. prefect/events/utilities.py +2 -3
  62. prefect/events/worker.py +2 -3
  63. prefect/filesystems.py +6 -517
  64. prefect/{new_flow_engine.py → flow_engine.py} +315 -74
  65. prefect/flow_runs.py +379 -7
  66. prefect/flows.py +248 -165
  67. prefect/futures.py +187 -345
  68. prefect/infrastructure/__init__.py +0 -27
  69. prefect/infrastructure/provisioners/__init__.py +5 -3
  70. prefect/infrastructure/provisioners/cloud_run.py +11 -6
  71. prefect/infrastructure/provisioners/container_instance.py +11 -7
  72. prefect/infrastructure/provisioners/ecs.py +6 -4
  73. prefect/infrastructure/provisioners/modal.py +8 -5
  74. prefect/input/actions.py +2 -4
  75. prefect/input/run_input.py +9 -9
  76. prefect/logging/formatters.py +0 -2
  77. prefect/logging/handlers.py +3 -11
  78. prefect/logging/loggers.py +2 -2
  79. prefect/manifests.py +2 -1
  80. prefect/records/__init__.py +1 -0
  81. prefect/records/cache_policies.py +179 -0
  82. prefect/records/result_store.py +42 -0
  83. prefect/records/store.py +9 -0
  84. prefect/results.py +43 -39
  85. prefect/runner/runner.py +9 -9
  86. prefect/runner/server.py +6 -10
  87. prefect/runner/storage.py +3 -8
  88. prefect/runner/submit.py +2 -2
  89. prefect/runner/utils.py +2 -2
  90. prefect/serializers.py +24 -35
  91. prefect/server/api/collections_data/views/aggregate-worker-metadata.json +5 -14
  92. prefect/settings.py +76 -136
  93. prefect/states.py +22 -50
  94. prefect/task_engine.py +666 -56
  95. prefect/task_runners.py +272 -300
  96. prefect/task_runs.py +203 -0
  97. prefect/{task_server.py → task_worker.py} +89 -60
  98. prefect/tasks.py +358 -341
  99. prefect/transactions.py +224 -0
  100. prefect/types/__init__.py +61 -82
  101. prefect/utilities/asyncutils.py +195 -136
  102. prefect/utilities/callables.py +121 -41
  103. prefect/utilities/collections.py +23 -38
  104. prefect/utilities/dispatch.py +11 -3
  105. prefect/utilities/dockerutils.py +4 -0
  106. prefect/utilities/engine.py +140 -20
  107. prefect/utilities/importtools.py +26 -27
  108. prefect/utilities/pydantic.py +128 -38
  109. prefect/utilities/schema_tools/hydration.py +5 -1
  110. prefect/utilities/templating.py +12 -2
  111. prefect/variables.py +84 -62
  112. prefect/workers/__init__.py +0 -1
  113. prefect/workers/base.py +26 -18
  114. prefect/workers/process.py +3 -8
  115. prefect/workers/server.py +2 -2
  116. {prefect_client-2.19.4.dist-info → prefect_client-3.0.0rc2.dist-info}/METADATA +23 -21
  117. prefect_client-3.0.0rc2.dist-info/RECORD +179 -0
  118. prefect/_internal/pydantic/_base_model.py +0 -51
  119. prefect/_internal/pydantic/_compat.py +0 -82
  120. prefect/_internal/pydantic/_flags.py +0 -20
  121. prefect/_internal/pydantic/_types.py +0 -8
  122. prefect/_internal/pydantic/utilities/__init__.py +0 -0
  123. prefect/_internal/pydantic/utilities/config_dict.py +0 -72
  124. prefect/_internal/pydantic/utilities/field_validator.py +0 -150
  125. prefect/_internal/pydantic/utilities/model_construct.py +0 -56
  126. prefect/_internal/pydantic/utilities/model_copy.py +0 -55
  127. prefect/_internal/pydantic/utilities/model_dump.py +0 -136
  128. prefect/_internal/pydantic/utilities/model_dump_json.py +0 -112
  129. prefect/_internal/pydantic/utilities/model_fields.py +0 -50
  130. prefect/_internal/pydantic/utilities/model_fields_set.py +0 -29
  131. prefect/_internal/pydantic/utilities/model_json_schema.py +0 -82
  132. prefect/_internal/pydantic/utilities/model_rebuild.py +0 -80
  133. prefect/_internal/pydantic/utilities/model_validate.py +0 -75
  134. prefect/_internal/pydantic/utilities/model_validate_json.py +0 -68
  135. prefect/_internal/pydantic/utilities/model_validator.py +0 -87
  136. prefect/_internal/pydantic/utilities/type_adapter.py +0 -71
  137. prefect/_vendor/__init__.py +0 -0
  138. prefect/_vendor/fastapi/__init__.py +0 -25
  139. prefect/_vendor/fastapi/applications.py +0 -946
  140. prefect/_vendor/fastapi/background.py +0 -3
  141. prefect/_vendor/fastapi/concurrency.py +0 -44
  142. prefect/_vendor/fastapi/datastructures.py +0 -58
  143. prefect/_vendor/fastapi/dependencies/__init__.py +0 -0
  144. prefect/_vendor/fastapi/dependencies/models.py +0 -64
  145. prefect/_vendor/fastapi/dependencies/utils.py +0 -877
  146. prefect/_vendor/fastapi/encoders.py +0 -177
  147. prefect/_vendor/fastapi/exception_handlers.py +0 -40
  148. prefect/_vendor/fastapi/exceptions.py +0 -46
  149. prefect/_vendor/fastapi/logger.py +0 -3
  150. prefect/_vendor/fastapi/middleware/__init__.py +0 -1
  151. prefect/_vendor/fastapi/middleware/asyncexitstack.py +0 -25
  152. prefect/_vendor/fastapi/middleware/cors.py +0 -3
  153. prefect/_vendor/fastapi/middleware/gzip.py +0 -3
  154. prefect/_vendor/fastapi/middleware/httpsredirect.py +0 -3
  155. prefect/_vendor/fastapi/middleware/trustedhost.py +0 -3
  156. prefect/_vendor/fastapi/middleware/wsgi.py +0 -3
  157. prefect/_vendor/fastapi/openapi/__init__.py +0 -0
  158. prefect/_vendor/fastapi/openapi/constants.py +0 -2
  159. prefect/_vendor/fastapi/openapi/docs.py +0 -203
  160. prefect/_vendor/fastapi/openapi/models.py +0 -480
  161. prefect/_vendor/fastapi/openapi/utils.py +0 -485
  162. prefect/_vendor/fastapi/param_functions.py +0 -340
  163. prefect/_vendor/fastapi/params.py +0 -453
  164. prefect/_vendor/fastapi/requests.py +0 -4
  165. prefect/_vendor/fastapi/responses.py +0 -40
  166. prefect/_vendor/fastapi/routing.py +0 -1331
  167. prefect/_vendor/fastapi/security/__init__.py +0 -15
  168. prefect/_vendor/fastapi/security/api_key.py +0 -98
  169. prefect/_vendor/fastapi/security/base.py +0 -6
  170. prefect/_vendor/fastapi/security/http.py +0 -172
  171. prefect/_vendor/fastapi/security/oauth2.py +0 -227
  172. prefect/_vendor/fastapi/security/open_id_connect_url.py +0 -34
  173. prefect/_vendor/fastapi/security/utils.py +0 -10
  174. prefect/_vendor/fastapi/staticfiles.py +0 -1
  175. prefect/_vendor/fastapi/templating.py +0 -3
  176. prefect/_vendor/fastapi/testclient.py +0 -1
  177. prefect/_vendor/fastapi/types.py +0 -3
  178. prefect/_vendor/fastapi/utils.py +0 -235
  179. prefect/_vendor/fastapi/websockets.py +0 -7
  180. prefect/_vendor/starlette/__init__.py +0 -1
  181. prefect/_vendor/starlette/_compat.py +0 -28
  182. prefect/_vendor/starlette/_exception_handler.py +0 -80
  183. prefect/_vendor/starlette/_utils.py +0 -88
  184. prefect/_vendor/starlette/applications.py +0 -261
  185. prefect/_vendor/starlette/authentication.py +0 -159
  186. prefect/_vendor/starlette/background.py +0 -43
  187. prefect/_vendor/starlette/concurrency.py +0 -59
  188. prefect/_vendor/starlette/config.py +0 -151
  189. prefect/_vendor/starlette/convertors.py +0 -87
  190. prefect/_vendor/starlette/datastructures.py +0 -707
  191. prefect/_vendor/starlette/endpoints.py +0 -130
  192. prefect/_vendor/starlette/exceptions.py +0 -60
  193. prefect/_vendor/starlette/formparsers.py +0 -276
  194. prefect/_vendor/starlette/middleware/__init__.py +0 -17
  195. prefect/_vendor/starlette/middleware/authentication.py +0 -52
  196. prefect/_vendor/starlette/middleware/base.py +0 -220
  197. prefect/_vendor/starlette/middleware/cors.py +0 -176
  198. prefect/_vendor/starlette/middleware/errors.py +0 -265
  199. prefect/_vendor/starlette/middleware/exceptions.py +0 -74
  200. prefect/_vendor/starlette/middleware/gzip.py +0 -113
  201. prefect/_vendor/starlette/middleware/httpsredirect.py +0 -19
  202. prefect/_vendor/starlette/middleware/sessions.py +0 -82
  203. prefect/_vendor/starlette/middleware/trustedhost.py +0 -64
  204. prefect/_vendor/starlette/middleware/wsgi.py +0 -147
  205. prefect/_vendor/starlette/requests.py +0 -328
  206. prefect/_vendor/starlette/responses.py +0 -347
  207. prefect/_vendor/starlette/routing.py +0 -933
  208. prefect/_vendor/starlette/schemas.py +0 -154
  209. prefect/_vendor/starlette/staticfiles.py +0 -248
  210. prefect/_vendor/starlette/status.py +0 -199
  211. prefect/_vendor/starlette/templating.py +0 -231
  212. prefect/_vendor/starlette/testclient.py +0 -804
  213. prefect/_vendor/starlette/types.py +0 -30
  214. prefect/_vendor/starlette/websockets.py +0 -193
  215. prefect/agent.py +0 -698
  216. prefect/deployments/deployments.py +0 -1042
  217. prefect/deprecated/__init__.py +0 -0
  218. prefect/deprecated/data_documents.py +0 -350
  219. prefect/deprecated/packaging/__init__.py +0 -12
  220. prefect/deprecated/packaging/base.py +0 -96
  221. prefect/deprecated/packaging/docker.py +0 -146
  222. prefect/deprecated/packaging/file.py +0 -92
  223. prefect/deprecated/packaging/orion.py +0 -80
  224. prefect/deprecated/packaging/serializers.py +0 -171
  225. prefect/events/instrument.py +0 -135
  226. prefect/infrastructure/base.py +0 -323
  227. prefect/infrastructure/container.py +0 -818
  228. prefect/infrastructure/kubernetes.py +0 -920
  229. prefect/infrastructure/process.py +0 -289
  230. prefect/new_task_engine.py +0 -423
  231. prefect/pydantic/__init__.py +0 -76
  232. prefect/pydantic/main.py +0 -39
  233. prefect/software/__init__.py +0 -2
  234. prefect/software/base.py +0 -50
  235. prefect/software/conda.py +0 -199
  236. prefect/software/pip.py +0 -122
  237. prefect/software/python.py +0 -52
  238. prefect/workers/block.py +0 -218
  239. prefect_client-2.19.4.dist-info/RECORD +0 -292
  240. {prefect_client-2.19.4.dist-info → prefect_client-3.0.0rc2.dist-info}/LICENSE +0 -0
  241. {prefect_client-2.19.4.dist-info → prefect_client-3.0.0rc2.dist-info}/WHEEL +0 -0
  242. {prefect_client-2.19.4.dist-info → prefect_client-3.0.0rc2.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)