prefect-client 2.20.4__py3-none-any.whl → 3.0.0__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 (288) hide show
  1. prefect/__init__.py +74 -110
  2. prefect/_internal/compatibility/deprecated.py +6 -115
  3. prefect/_internal/compatibility/experimental.py +4 -79
  4. prefect/_internal/compatibility/migration.py +166 -0
  5. prefect/_internal/concurrency/__init__.py +2 -2
  6. prefect/_internal/concurrency/api.py +1 -35
  7. prefect/_internal/concurrency/calls.py +0 -6
  8. prefect/_internal/concurrency/cancellation.py +0 -3
  9. prefect/_internal/concurrency/event_loop.py +0 -20
  10. prefect/_internal/concurrency/inspection.py +3 -3
  11. prefect/_internal/concurrency/primitives.py +1 -0
  12. prefect/_internal/concurrency/services.py +23 -0
  13. prefect/_internal/concurrency/threads.py +35 -0
  14. prefect/_internal/concurrency/waiters.py +0 -28
  15. prefect/_internal/integrations.py +7 -0
  16. prefect/_internal/pydantic/__init__.py +0 -45
  17. prefect/_internal/pydantic/annotations/pendulum.py +2 -2
  18. prefect/_internal/pydantic/v1_schema.py +21 -22
  19. prefect/_internal/pydantic/v2_schema.py +0 -2
  20. prefect/_internal/pydantic/v2_validated_func.py +18 -23
  21. prefect/_internal/pytz.py +1 -1
  22. prefect/_internal/retries.py +61 -0
  23. prefect/_internal/schemas/bases.py +45 -177
  24. prefect/_internal/schemas/fields.py +1 -43
  25. prefect/_internal/schemas/validators.py +47 -233
  26. prefect/agent.py +3 -695
  27. prefect/artifacts.py +173 -14
  28. prefect/automations.py +39 -4
  29. prefect/blocks/abstract.py +1 -1
  30. prefect/blocks/core.py +405 -153
  31. prefect/blocks/fields.py +2 -57
  32. prefect/blocks/notifications.py +43 -28
  33. prefect/blocks/redis.py +168 -0
  34. prefect/blocks/system.py +67 -20
  35. prefect/blocks/webhook.py +2 -9
  36. prefect/cache_policies.py +239 -0
  37. prefect/client/__init__.py +4 -0
  38. prefect/client/base.py +33 -27
  39. prefect/client/cloud.py +65 -20
  40. prefect/client/collections.py +1 -1
  41. prefect/client/orchestration.py +650 -442
  42. prefect/client/schemas/actions.py +115 -100
  43. prefect/client/schemas/filters.py +46 -52
  44. prefect/client/schemas/objects.py +228 -178
  45. prefect/client/schemas/responses.py +18 -36
  46. prefect/client/schemas/schedules.py +55 -36
  47. prefect/client/schemas/sorting.py +2 -0
  48. prefect/client/subscriptions.py +8 -7
  49. prefect/client/types/flexible_schedule_list.py +11 -0
  50. prefect/client/utilities.py +9 -6
  51. prefect/concurrency/asyncio.py +60 -11
  52. prefect/concurrency/context.py +24 -0
  53. prefect/concurrency/events.py +2 -2
  54. prefect/concurrency/services.py +46 -16
  55. prefect/concurrency/sync.py +51 -7
  56. prefect/concurrency/v1/asyncio.py +143 -0
  57. prefect/concurrency/v1/context.py +27 -0
  58. prefect/concurrency/v1/events.py +61 -0
  59. prefect/concurrency/v1/services.py +116 -0
  60. prefect/concurrency/v1/sync.py +92 -0
  61. prefect/context.py +246 -149
  62. prefect/deployments/__init__.py +33 -18
  63. prefect/deployments/base.py +10 -15
  64. prefect/deployments/deployments.py +2 -1048
  65. prefect/deployments/flow_runs.py +178 -0
  66. prefect/deployments/runner.py +72 -173
  67. prefect/deployments/schedules.py +31 -25
  68. prefect/deployments/steps/__init__.py +0 -1
  69. prefect/deployments/steps/core.py +7 -0
  70. prefect/deployments/steps/pull.py +15 -21
  71. prefect/deployments/steps/utility.py +2 -1
  72. prefect/docker/__init__.py +20 -0
  73. prefect/docker/docker_image.py +82 -0
  74. prefect/engine.py +15 -2475
  75. prefect/events/actions.py +17 -23
  76. prefect/events/cli/automations.py +20 -7
  77. prefect/events/clients.py +142 -80
  78. prefect/events/filters.py +14 -18
  79. prefect/events/related.py +74 -75
  80. prefect/events/schemas/__init__.py +0 -5
  81. prefect/events/schemas/automations.py +55 -46
  82. prefect/events/schemas/deployment_triggers.py +7 -197
  83. prefect/events/schemas/events.py +46 -65
  84. prefect/events/schemas/labelling.py +10 -14
  85. prefect/events/utilities.py +4 -5
  86. prefect/events/worker.py +23 -8
  87. prefect/exceptions.py +15 -0
  88. prefect/filesystems.py +30 -529
  89. prefect/flow_engine.py +827 -0
  90. prefect/flow_runs.py +379 -7
  91. prefect/flows.py +470 -360
  92. prefect/futures.py +382 -331
  93. prefect/infrastructure/__init__.py +5 -26
  94. prefect/infrastructure/base.py +3 -320
  95. prefect/infrastructure/provisioners/__init__.py +5 -3
  96. prefect/infrastructure/provisioners/cloud_run.py +13 -8
  97. prefect/infrastructure/provisioners/container_instance.py +14 -9
  98. prefect/infrastructure/provisioners/ecs.py +10 -8
  99. prefect/infrastructure/provisioners/modal.py +8 -5
  100. prefect/input/__init__.py +4 -0
  101. prefect/input/actions.py +2 -4
  102. prefect/input/run_input.py +9 -9
  103. prefect/logging/formatters.py +2 -4
  104. prefect/logging/handlers.py +9 -14
  105. prefect/logging/loggers.py +5 -5
  106. prefect/main.py +72 -0
  107. prefect/plugins.py +2 -64
  108. prefect/profiles.toml +16 -2
  109. prefect/records/__init__.py +1 -0
  110. prefect/records/base.py +223 -0
  111. prefect/records/filesystem.py +207 -0
  112. prefect/records/memory.py +178 -0
  113. prefect/records/result_store.py +64 -0
  114. prefect/results.py +577 -504
  115. prefect/runner/runner.py +117 -47
  116. prefect/runner/server.py +32 -34
  117. prefect/runner/storage.py +3 -12
  118. prefect/runner/submit.py +2 -10
  119. prefect/runner/utils.py +2 -2
  120. prefect/runtime/__init__.py +1 -0
  121. prefect/runtime/deployment.py +1 -0
  122. prefect/runtime/flow_run.py +40 -5
  123. prefect/runtime/task_run.py +1 -0
  124. prefect/serializers.py +28 -39
  125. prefect/server/api/collections_data/views/aggregate-worker-metadata.json +5 -14
  126. prefect/settings.py +209 -332
  127. prefect/states.py +160 -63
  128. prefect/task_engine.py +1478 -57
  129. prefect/task_runners.py +383 -287
  130. prefect/task_runs.py +240 -0
  131. prefect/task_worker.py +463 -0
  132. prefect/tasks.py +684 -374
  133. prefect/transactions.py +410 -0
  134. prefect/types/__init__.py +72 -86
  135. prefect/types/entrypoint.py +13 -0
  136. prefect/utilities/annotations.py +4 -3
  137. prefect/utilities/asyncutils.py +227 -148
  138. prefect/utilities/callables.py +137 -45
  139. prefect/utilities/collections.py +134 -86
  140. prefect/utilities/dispatch.py +27 -14
  141. prefect/utilities/dockerutils.py +11 -4
  142. prefect/utilities/engine.py +186 -32
  143. prefect/utilities/filesystem.py +4 -5
  144. prefect/utilities/importtools.py +26 -27
  145. prefect/utilities/pydantic.py +128 -38
  146. prefect/utilities/schema_tools/hydration.py +18 -1
  147. prefect/utilities/schema_tools/validation.py +30 -0
  148. prefect/utilities/services.py +35 -9
  149. prefect/utilities/templating.py +12 -2
  150. prefect/utilities/timeout.py +20 -5
  151. prefect/utilities/urls.py +195 -0
  152. prefect/utilities/visualization.py +1 -0
  153. prefect/variables.py +78 -59
  154. prefect/workers/__init__.py +0 -1
  155. prefect/workers/base.py +237 -244
  156. prefect/workers/block.py +5 -226
  157. prefect/workers/cloud.py +6 -0
  158. prefect/workers/process.py +265 -12
  159. prefect/workers/server.py +29 -11
  160. {prefect_client-2.20.4.dist-info → prefect_client-3.0.0.dist-info}/METADATA +28 -24
  161. prefect_client-3.0.0.dist-info/RECORD +201 -0
  162. {prefect_client-2.20.4.dist-info → prefect_client-3.0.0.dist-info}/WHEEL +1 -1
  163. prefect/_internal/pydantic/_base_model.py +0 -51
  164. prefect/_internal/pydantic/_compat.py +0 -82
  165. prefect/_internal/pydantic/_flags.py +0 -20
  166. prefect/_internal/pydantic/_types.py +0 -8
  167. prefect/_internal/pydantic/utilities/config_dict.py +0 -72
  168. prefect/_internal/pydantic/utilities/field_validator.py +0 -150
  169. prefect/_internal/pydantic/utilities/model_construct.py +0 -56
  170. prefect/_internal/pydantic/utilities/model_copy.py +0 -55
  171. prefect/_internal/pydantic/utilities/model_dump.py +0 -136
  172. prefect/_internal/pydantic/utilities/model_dump_json.py +0 -112
  173. prefect/_internal/pydantic/utilities/model_fields.py +0 -50
  174. prefect/_internal/pydantic/utilities/model_fields_set.py +0 -29
  175. prefect/_internal/pydantic/utilities/model_json_schema.py +0 -82
  176. prefect/_internal/pydantic/utilities/model_rebuild.py +0 -80
  177. prefect/_internal/pydantic/utilities/model_validate.py +0 -75
  178. prefect/_internal/pydantic/utilities/model_validate_json.py +0 -68
  179. prefect/_internal/pydantic/utilities/model_validator.py +0 -87
  180. prefect/_internal/pydantic/utilities/type_adapter.py +0 -71
  181. prefect/_vendor/fastapi/__init__.py +0 -25
  182. prefect/_vendor/fastapi/applications.py +0 -946
  183. prefect/_vendor/fastapi/background.py +0 -3
  184. prefect/_vendor/fastapi/concurrency.py +0 -44
  185. prefect/_vendor/fastapi/datastructures.py +0 -58
  186. prefect/_vendor/fastapi/dependencies/__init__.py +0 -0
  187. prefect/_vendor/fastapi/dependencies/models.py +0 -64
  188. prefect/_vendor/fastapi/dependencies/utils.py +0 -877
  189. prefect/_vendor/fastapi/encoders.py +0 -177
  190. prefect/_vendor/fastapi/exception_handlers.py +0 -40
  191. prefect/_vendor/fastapi/exceptions.py +0 -46
  192. prefect/_vendor/fastapi/logger.py +0 -3
  193. prefect/_vendor/fastapi/middleware/__init__.py +0 -1
  194. prefect/_vendor/fastapi/middleware/asyncexitstack.py +0 -25
  195. prefect/_vendor/fastapi/middleware/cors.py +0 -3
  196. prefect/_vendor/fastapi/middleware/gzip.py +0 -3
  197. prefect/_vendor/fastapi/middleware/httpsredirect.py +0 -3
  198. prefect/_vendor/fastapi/middleware/trustedhost.py +0 -3
  199. prefect/_vendor/fastapi/middleware/wsgi.py +0 -3
  200. prefect/_vendor/fastapi/openapi/__init__.py +0 -0
  201. prefect/_vendor/fastapi/openapi/constants.py +0 -2
  202. prefect/_vendor/fastapi/openapi/docs.py +0 -203
  203. prefect/_vendor/fastapi/openapi/models.py +0 -480
  204. prefect/_vendor/fastapi/openapi/utils.py +0 -485
  205. prefect/_vendor/fastapi/param_functions.py +0 -340
  206. prefect/_vendor/fastapi/params.py +0 -453
  207. prefect/_vendor/fastapi/py.typed +0 -0
  208. prefect/_vendor/fastapi/requests.py +0 -4
  209. prefect/_vendor/fastapi/responses.py +0 -40
  210. prefect/_vendor/fastapi/routing.py +0 -1331
  211. prefect/_vendor/fastapi/security/__init__.py +0 -15
  212. prefect/_vendor/fastapi/security/api_key.py +0 -98
  213. prefect/_vendor/fastapi/security/base.py +0 -6
  214. prefect/_vendor/fastapi/security/http.py +0 -172
  215. prefect/_vendor/fastapi/security/oauth2.py +0 -227
  216. prefect/_vendor/fastapi/security/open_id_connect_url.py +0 -34
  217. prefect/_vendor/fastapi/security/utils.py +0 -10
  218. prefect/_vendor/fastapi/staticfiles.py +0 -1
  219. prefect/_vendor/fastapi/templating.py +0 -3
  220. prefect/_vendor/fastapi/testclient.py +0 -1
  221. prefect/_vendor/fastapi/types.py +0 -3
  222. prefect/_vendor/fastapi/utils.py +0 -235
  223. prefect/_vendor/fastapi/websockets.py +0 -7
  224. prefect/_vendor/starlette/__init__.py +0 -1
  225. prefect/_vendor/starlette/_compat.py +0 -28
  226. prefect/_vendor/starlette/_exception_handler.py +0 -80
  227. prefect/_vendor/starlette/_utils.py +0 -88
  228. prefect/_vendor/starlette/applications.py +0 -261
  229. prefect/_vendor/starlette/authentication.py +0 -159
  230. prefect/_vendor/starlette/background.py +0 -43
  231. prefect/_vendor/starlette/concurrency.py +0 -59
  232. prefect/_vendor/starlette/config.py +0 -151
  233. prefect/_vendor/starlette/convertors.py +0 -87
  234. prefect/_vendor/starlette/datastructures.py +0 -707
  235. prefect/_vendor/starlette/endpoints.py +0 -130
  236. prefect/_vendor/starlette/exceptions.py +0 -60
  237. prefect/_vendor/starlette/formparsers.py +0 -276
  238. prefect/_vendor/starlette/middleware/__init__.py +0 -17
  239. prefect/_vendor/starlette/middleware/authentication.py +0 -52
  240. prefect/_vendor/starlette/middleware/base.py +0 -220
  241. prefect/_vendor/starlette/middleware/cors.py +0 -176
  242. prefect/_vendor/starlette/middleware/errors.py +0 -265
  243. prefect/_vendor/starlette/middleware/exceptions.py +0 -74
  244. prefect/_vendor/starlette/middleware/gzip.py +0 -113
  245. prefect/_vendor/starlette/middleware/httpsredirect.py +0 -19
  246. prefect/_vendor/starlette/middleware/sessions.py +0 -82
  247. prefect/_vendor/starlette/middleware/trustedhost.py +0 -64
  248. prefect/_vendor/starlette/middleware/wsgi.py +0 -147
  249. prefect/_vendor/starlette/py.typed +0 -0
  250. prefect/_vendor/starlette/requests.py +0 -328
  251. prefect/_vendor/starlette/responses.py +0 -347
  252. prefect/_vendor/starlette/routing.py +0 -933
  253. prefect/_vendor/starlette/schemas.py +0 -154
  254. prefect/_vendor/starlette/staticfiles.py +0 -248
  255. prefect/_vendor/starlette/status.py +0 -199
  256. prefect/_vendor/starlette/templating.py +0 -231
  257. prefect/_vendor/starlette/testclient.py +0 -804
  258. prefect/_vendor/starlette/types.py +0 -30
  259. prefect/_vendor/starlette/websockets.py +0 -193
  260. prefect/blocks/kubernetes.py +0 -119
  261. prefect/deprecated/__init__.py +0 -0
  262. prefect/deprecated/data_documents.py +0 -350
  263. prefect/deprecated/packaging/__init__.py +0 -12
  264. prefect/deprecated/packaging/base.py +0 -96
  265. prefect/deprecated/packaging/docker.py +0 -146
  266. prefect/deprecated/packaging/file.py +0 -92
  267. prefect/deprecated/packaging/orion.py +0 -80
  268. prefect/deprecated/packaging/serializers.py +0 -171
  269. prefect/events/instrument.py +0 -135
  270. prefect/infrastructure/container.py +0 -824
  271. prefect/infrastructure/kubernetes.py +0 -920
  272. prefect/infrastructure/process.py +0 -289
  273. prefect/manifests.py +0 -20
  274. prefect/new_flow_engine.py +0 -449
  275. prefect/new_task_engine.py +0 -423
  276. prefect/pydantic/__init__.py +0 -76
  277. prefect/pydantic/main.py +0 -39
  278. prefect/software/__init__.py +0 -2
  279. prefect/software/base.py +0 -50
  280. prefect/software/conda.py +0 -199
  281. prefect/software/pip.py +0 -122
  282. prefect/software/python.py +0 -52
  283. prefect/task_server.py +0 -322
  284. prefect_client-2.20.4.dist-info/RECORD +0 -294
  285. /prefect/{_internal/pydantic/utilities → client/types}/__init__.py +0 -0
  286. /prefect/{_vendor → concurrency/v1}/__init__.py +0 -0
  287. {prefect_client-2.20.4.dist-info → prefect_client-3.0.0.dist-info}/LICENSE +0 -0
  288. {prefect_client-2.20.4.dist-info → prefect_client-3.0.0.dist-info}/top_level.txt +0 -0
@@ -1,328 +0,0 @@
1
- import json
2
- import typing
3
- from http import cookies as http_cookies
4
-
5
- import anyio
6
- from prefect._vendor.starlette._utils import (
7
- AwaitableOrContextManager,
8
- AwaitableOrContextManagerWrapper,
9
- )
10
- from prefect._vendor.starlette.datastructures import (
11
- URL,
12
- Address,
13
- FormData,
14
- Headers,
15
- QueryParams,
16
- State,
17
- )
18
- from prefect._vendor.starlette.exceptions import HTTPException
19
- from prefect._vendor.starlette.formparsers import (
20
- FormParser,
21
- MultiPartException,
22
- MultiPartParser,
23
- )
24
- from prefect._vendor.starlette.types import Message, Receive, Scope, Send
25
-
26
- try:
27
- from multipart.multipart import parse_options_header
28
- except ModuleNotFoundError: # pragma: nocover
29
- parse_options_header = None
30
-
31
-
32
- if typing.TYPE_CHECKING:
33
- from prefect._vendor.starlette.routing import Router
34
-
35
-
36
- SERVER_PUSH_HEADERS_TO_COPY = {
37
- "accept",
38
- "accept-encoding",
39
- "accept-language",
40
- "cache-control",
41
- "user-agent",
42
- }
43
-
44
-
45
- def cookie_parser(cookie_string: str) -> typing.Dict[str, str]:
46
- """
47
- This function parses a ``Cookie`` HTTP header into a dict of key/value pairs.
48
-
49
- It attempts to mimic browser cookie parsing behavior: browsers and web servers
50
- frequently disregard the spec (RFC 6265) when setting and reading cookies,
51
- so we attempt to suit the common scenarios here.
52
-
53
- This function has been adapted from Django 3.1.0.
54
- Note: we are explicitly _NOT_ using `SimpleCookie.load` because it is based
55
- on an outdated spec and will fail on lots of input we want to support
56
- """
57
- cookie_dict: typing.Dict[str, str] = {}
58
- for chunk in cookie_string.split(";"):
59
- if "=" in chunk:
60
- key, val = chunk.split("=", 1)
61
- else:
62
- # Assume an empty name per
63
- # https://bugzilla.mozilla.org/show_bug.cgi?id=169091
64
- key, val = "", chunk
65
- key, val = key.strip(), val.strip()
66
- if key or val:
67
- # unquote using Python's algorithm.
68
- cookie_dict[key] = http_cookies._unquote(val)
69
- return cookie_dict
70
-
71
-
72
- class ClientDisconnect(Exception):
73
- pass
74
-
75
-
76
- class HTTPConnection(typing.Mapping[str, typing.Any]):
77
- """
78
- A base class for incoming HTTP connections, that is used to provide
79
- any functionality that is common to both `Request` and `WebSocket`.
80
- """
81
-
82
- def __init__(self, scope: Scope, receive: typing.Optional[Receive] = None) -> None:
83
- assert scope["type"] in ("http", "websocket")
84
- self.scope = scope
85
-
86
- def __getitem__(self, key: str) -> typing.Any:
87
- return self.scope[key]
88
-
89
- def __iter__(self) -> typing.Iterator[str]:
90
- return iter(self.scope)
91
-
92
- def __len__(self) -> int:
93
- return len(self.scope)
94
-
95
- # Don't use the `abc.Mapping.__eq__` implementation.
96
- # Connection instances should never be considered equal
97
- # unless `self is other`.
98
- __eq__ = object.__eq__
99
- __hash__ = object.__hash__
100
-
101
- @property
102
- def app(self) -> typing.Any:
103
- return self.scope["app"]
104
-
105
- @property
106
- def url(self) -> URL:
107
- if not hasattr(self, "_url"):
108
- self._url = URL(scope=self.scope)
109
- return self._url
110
-
111
- @property
112
- def base_url(self) -> URL:
113
- if not hasattr(self, "_base_url"):
114
- base_url_scope = dict(self.scope)
115
- base_url_scope["path"] = "/"
116
- base_url_scope["query_string"] = b""
117
- base_url_scope["root_path"] = base_url_scope.get("root_path", "")
118
- self._base_url = URL(scope=base_url_scope)
119
- return self._base_url
120
-
121
- @property
122
- def headers(self) -> Headers:
123
- if not hasattr(self, "_headers"):
124
- self._headers = Headers(scope=self.scope)
125
- return self._headers
126
-
127
- @property
128
- def query_params(self) -> QueryParams:
129
- if not hasattr(self, "_query_params"):
130
- self._query_params = QueryParams(self.scope["query_string"])
131
- return self._query_params
132
-
133
- @property
134
- def path_params(self) -> typing.Dict[str, typing.Any]:
135
- return self.scope.get("path_params", {})
136
-
137
- @property
138
- def cookies(self) -> typing.Dict[str, str]:
139
- if not hasattr(self, "_cookies"):
140
- cookies: typing.Dict[str, str] = {}
141
- cookie_header = self.headers.get("cookie")
142
-
143
- if cookie_header:
144
- cookies = cookie_parser(cookie_header)
145
- self._cookies = cookies
146
- return self._cookies
147
-
148
- @property
149
- def client(self) -> typing.Optional[Address]:
150
- # client is a 2 item tuple of (host, port), None or missing
151
- host_port = self.scope.get("client")
152
- if host_port is not None:
153
- return Address(*host_port)
154
- return None
155
-
156
- @property
157
- def session(self) -> typing.Dict[str, typing.Any]:
158
- assert (
159
- "session" in self.scope
160
- ), "SessionMiddleware must be installed to access request.session"
161
- return self.scope["session"] # type: ignore[no-any-return]
162
-
163
- @property
164
- def auth(self) -> typing.Any:
165
- assert (
166
- "auth" in self.scope
167
- ), "AuthenticationMiddleware must be installed to access request.auth"
168
- return self.scope["auth"]
169
-
170
- @property
171
- def user(self) -> typing.Any:
172
- assert (
173
- "user" in self.scope
174
- ), "AuthenticationMiddleware must be installed to access request.user"
175
- return self.scope["user"]
176
-
177
- @property
178
- def state(self) -> State:
179
- if not hasattr(self, "_state"):
180
- # Ensure 'state' has an empty dict if it's not already populated.
181
- self.scope.setdefault("state", {})
182
- # Create a state instance with a reference to the dict in which it should
183
- # store info
184
- self._state = State(self.scope["state"])
185
- return self._state
186
-
187
- def url_for(self, name: str, /, **path_params: typing.Any) -> URL:
188
- router: Router = self.scope["router"]
189
- url_path = router.url_path_for(name, **path_params)
190
- return url_path.make_absolute_url(base_url=self.base_url)
191
-
192
-
193
- async def empty_receive() -> typing.NoReturn:
194
- raise RuntimeError("Receive channel has not been made available")
195
-
196
-
197
- async def empty_send(message: Message) -> typing.NoReturn:
198
- raise RuntimeError("Send channel has not been made available")
199
-
200
-
201
- class Request(HTTPConnection):
202
- _form: typing.Optional[FormData]
203
-
204
- def __init__(
205
- self, scope: Scope, receive: Receive = empty_receive, send: Send = empty_send
206
- ):
207
- super().__init__(scope)
208
- assert scope["type"] == "http"
209
- self._receive = receive
210
- self._send = send
211
- self._stream_consumed = False
212
- self._is_disconnected = False
213
- self._form = None
214
-
215
- @property
216
- def method(self) -> str:
217
- return typing.cast(str, self.scope["method"])
218
-
219
- @property
220
- def receive(self) -> Receive:
221
- return self._receive
222
-
223
- async def stream(self) -> typing.AsyncGenerator[bytes, None]:
224
- if hasattr(self, "_body"):
225
- yield self._body
226
- yield b""
227
- return
228
- if self._stream_consumed:
229
- raise RuntimeError("Stream consumed")
230
- while not self._stream_consumed:
231
- message = await self._receive()
232
- if message["type"] == "http.request":
233
- body = message.get("body", b"")
234
- if not message.get("more_body", False):
235
- self._stream_consumed = True
236
- if body:
237
- yield body
238
- elif message["type"] == "http.disconnect":
239
- self._is_disconnected = True
240
- raise ClientDisconnect()
241
- yield b""
242
-
243
- async def body(self) -> bytes:
244
- if not hasattr(self, "_body"):
245
- chunks: "typing.List[bytes]" = []
246
- async for chunk in self.stream():
247
- chunks.append(chunk)
248
- self._body = b"".join(chunks)
249
- return self._body
250
-
251
- async def json(self) -> typing.Any:
252
- if not hasattr(self, "_json"):
253
- body = await self.body()
254
- self._json = json.loads(body)
255
- return self._json
256
-
257
- async def _get_form(
258
- self,
259
- *,
260
- max_files: typing.Union[int, float] = 1000,
261
- max_fields: typing.Union[int, float] = 1000,
262
- ) -> FormData:
263
- if self._form is None:
264
- assert (
265
- parse_options_header is not None
266
- ), "The `python-multipart` library must be installed to use form parsing."
267
- content_type_header = self.headers.get("Content-Type")
268
- content_type: bytes
269
- content_type, _ = parse_options_header(content_type_header)
270
- if content_type == b"multipart/form-data":
271
- try:
272
- multipart_parser = MultiPartParser(
273
- self.headers,
274
- self.stream(),
275
- max_files=max_files,
276
- max_fields=max_fields,
277
- )
278
- self._form = await multipart_parser.parse()
279
- except MultiPartException as exc:
280
- if "app" in self.scope:
281
- raise HTTPException(status_code=400, detail=exc.message)
282
- raise exc
283
- elif content_type == b"application/x-www-form-urlencoded":
284
- form_parser = FormParser(self.headers, self.stream())
285
- self._form = await form_parser.parse()
286
- else:
287
- self._form = FormData()
288
- return self._form
289
-
290
- def form(
291
- self,
292
- *,
293
- max_files: typing.Union[int, float] = 1000,
294
- max_fields: typing.Union[int, float] = 1000,
295
- ) -> AwaitableOrContextManager[FormData]:
296
- return AwaitableOrContextManagerWrapper(
297
- self._get_form(max_files=max_files, max_fields=max_fields)
298
- )
299
-
300
- async def close(self) -> None:
301
- if self._form is not None:
302
- await self._form.close()
303
-
304
- async def is_disconnected(self) -> bool:
305
- if not self._is_disconnected:
306
- message: Message = {}
307
-
308
- # If message isn't immediately available, move on
309
- with anyio.CancelScope() as cs:
310
- cs.cancel()
311
- message = await self._receive()
312
-
313
- if message.get("type") == "http.disconnect":
314
- self._is_disconnected = True
315
-
316
- return self._is_disconnected
317
-
318
- async def send_push_promise(self, path: str) -> None:
319
- if "http.response.push" in self.scope.get("extensions", {}):
320
- raw_headers: "typing.List[typing.Tuple[bytes, bytes]]" = []
321
- for name in SERVER_PUSH_HEADERS_TO_COPY:
322
- for value in self.headers.getlist(name):
323
- raw_headers.append(
324
- (name.encode("latin-1"), value.encode("latin-1"))
325
- )
326
- await self._send(
327
- {"type": "http.response.push", "path": path, "headers": raw_headers}
328
- )
@@ -1,347 +0,0 @@
1
- import http.cookies
2
- import json
3
- import os
4
- import stat
5
- import typing
6
- from datetime import datetime
7
- from email.utils import format_datetime, formatdate
8
- from functools import partial
9
- from mimetypes import guess_type
10
- from urllib.parse import quote
11
-
12
- import anyio
13
- from prefect._vendor.starlette._compat import md5_hexdigest
14
- from prefect._vendor.starlette.background import BackgroundTask
15
- from prefect._vendor.starlette.concurrency import iterate_in_threadpool
16
- from prefect._vendor.starlette.datastructures import URL, MutableHeaders
17
- from prefect._vendor.starlette.types import Receive, Scope, Send
18
-
19
-
20
- class Response:
21
- media_type = None
22
- charset = "utf-8"
23
-
24
- def __init__(
25
- self,
26
- content: typing.Any = None,
27
- status_code: int = 200,
28
- headers: typing.Optional[typing.Mapping[str, str]] = None,
29
- media_type: typing.Optional[str] = None,
30
- background: typing.Optional[BackgroundTask] = None,
31
- ) -> None:
32
- self.status_code = status_code
33
- if media_type is not None:
34
- self.media_type = media_type
35
- self.background = background
36
- self.body = self.render(content)
37
- self.init_headers(headers)
38
-
39
- def render(self, content: typing.Any) -> bytes:
40
- if content is None:
41
- return b""
42
- if isinstance(content, bytes):
43
- return content
44
- return content.encode(self.charset) # type: ignore
45
-
46
- def init_headers(
47
- self, headers: typing.Optional[typing.Mapping[str, str]] = None
48
- ) -> None:
49
- if headers is None:
50
- raw_headers: typing.List[typing.Tuple[bytes, bytes]] = []
51
- populate_content_length = True
52
- populate_content_type = True
53
- else:
54
- raw_headers = [
55
- (k.lower().encode("latin-1"), v.encode("latin-1"))
56
- for k, v in headers.items()
57
- ]
58
- keys = [h[0] for h in raw_headers]
59
- populate_content_length = b"content-length" not in keys
60
- populate_content_type = b"content-type" not in keys
61
-
62
- body = getattr(self, "body", None)
63
- if (
64
- body is not None
65
- and populate_content_length
66
- and not (self.status_code < 200 or self.status_code in (204, 304))
67
- ):
68
- content_length = str(len(body))
69
- raw_headers.append((b"content-length", content_length.encode("latin-1")))
70
-
71
- content_type = self.media_type
72
- if content_type is not None and populate_content_type:
73
- if content_type.startswith("text/"):
74
- content_type += "; charset=" + self.charset
75
- raw_headers.append((b"content-type", content_type.encode("latin-1")))
76
-
77
- self.raw_headers = raw_headers
78
-
79
- @property
80
- def headers(self) -> MutableHeaders:
81
- if not hasattr(self, "_headers"):
82
- self._headers = MutableHeaders(raw=self.raw_headers)
83
- return self._headers
84
-
85
- def set_cookie(
86
- self,
87
- key: str,
88
- value: str = "",
89
- max_age: typing.Optional[int] = None,
90
- expires: typing.Optional[typing.Union[datetime, str, int]] = None,
91
- path: str = "/",
92
- domain: typing.Optional[str] = None,
93
- secure: bool = False,
94
- httponly: bool = False,
95
- samesite: typing.Optional[typing.Literal["lax", "strict", "none"]] = "lax",
96
- ) -> None:
97
- cookie: "http.cookies.BaseCookie[str]" = http.cookies.SimpleCookie()
98
- cookie[key] = value
99
- if max_age is not None:
100
- cookie[key]["max-age"] = max_age
101
- if expires is not None:
102
- if isinstance(expires, datetime):
103
- cookie[key]["expires"] = format_datetime(expires, usegmt=True)
104
- else:
105
- cookie[key]["expires"] = expires
106
- if path is not None:
107
- cookie[key]["path"] = path
108
- if domain is not None:
109
- cookie[key]["domain"] = domain
110
- if secure:
111
- cookie[key]["secure"] = True
112
- if httponly:
113
- cookie[key]["httponly"] = True
114
- if samesite is not None:
115
- assert samesite.lower() in [
116
- "strict",
117
- "lax",
118
- "none",
119
- ], "samesite must be either 'strict', 'lax' or 'none'"
120
- cookie[key]["samesite"] = samesite
121
- cookie_val = cookie.output(header="").strip()
122
- self.raw_headers.append((b"set-cookie", cookie_val.encode("latin-1")))
123
-
124
- def delete_cookie(
125
- self,
126
- key: str,
127
- path: str = "/",
128
- domain: typing.Optional[str] = None,
129
- secure: bool = False,
130
- httponly: bool = False,
131
- samesite: typing.Optional[typing.Literal["lax", "strict", "none"]] = "lax",
132
- ) -> None:
133
- self.set_cookie(
134
- key,
135
- max_age=0,
136
- expires=0,
137
- path=path,
138
- domain=domain,
139
- secure=secure,
140
- httponly=httponly,
141
- samesite=samesite,
142
- )
143
-
144
- async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
145
- await send(
146
- {
147
- "type": "http.response.start",
148
- "status": self.status_code,
149
- "headers": self.raw_headers,
150
- }
151
- )
152
- await send({"type": "http.response.body", "body": self.body})
153
-
154
- if self.background is not None:
155
- await self.background()
156
-
157
-
158
- class HTMLResponse(Response):
159
- media_type = "text/html"
160
-
161
-
162
- class PlainTextResponse(Response):
163
- media_type = "text/plain"
164
-
165
-
166
- class JSONResponse(Response):
167
- media_type = "application/json"
168
-
169
- def __init__(
170
- self,
171
- content: typing.Any,
172
- status_code: int = 200,
173
- headers: typing.Optional[typing.Mapping[str, str]] = None,
174
- media_type: typing.Optional[str] = None,
175
- background: typing.Optional[BackgroundTask] = None,
176
- ) -> None:
177
- super().__init__(content, status_code, headers, media_type, background)
178
-
179
- def render(self, content: typing.Any) -> bytes:
180
- return json.dumps(
181
- content,
182
- ensure_ascii=False,
183
- allow_nan=False,
184
- indent=None,
185
- separators=(",", ":"),
186
- ).encode("utf-8")
187
-
188
-
189
- class RedirectResponse(Response):
190
- def __init__(
191
- self,
192
- url: typing.Union[str, URL],
193
- status_code: int = 307,
194
- headers: typing.Optional[typing.Mapping[str, str]] = None,
195
- background: typing.Optional[BackgroundTask] = None,
196
- ) -> None:
197
- super().__init__(
198
- content=b"", status_code=status_code, headers=headers, background=background
199
- )
200
- self.headers["location"] = quote(str(url), safe=":/%#?=@[]!$&'()*+,;")
201
-
202
-
203
- Content = typing.Union[str, bytes]
204
- SyncContentStream = typing.Iterator[Content]
205
- AsyncContentStream = typing.AsyncIterable[Content]
206
- ContentStream = typing.Union[AsyncContentStream, SyncContentStream]
207
-
208
-
209
- class StreamingResponse(Response):
210
- body_iterator: AsyncContentStream
211
-
212
- def __init__(
213
- self,
214
- content: ContentStream,
215
- status_code: int = 200,
216
- headers: typing.Optional[typing.Mapping[str, str]] = None,
217
- media_type: typing.Optional[str] = None,
218
- background: typing.Optional[BackgroundTask] = None,
219
- ) -> None:
220
- if isinstance(content, typing.AsyncIterable):
221
- self.body_iterator = content
222
- else:
223
- self.body_iterator = iterate_in_threadpool(content)
224
- self.status_code = status_code
225
- self.media_type = self.media_type if media_type is None else media_type
226
- self.background = background
227
- self.init_headers(headers)
228
-
229
- async def listen_for_disconnect(self, receive: Receive) -> None:
230
- while True:
231
- message = await receive()
232
- if message["type"] == "http.disconnect":
233
- break
234
-
235
- async def stream_response(self, send: Send) -> None:
236
- await send(
237
- {
238
- "type": "http.response.start",
239
- "status": self.status_code,
240
- "headers": self.raw_headers,
241
- }
242
- )
243
- async for chunk in self.body_iterator:
244
- if not isinstance(chunk, bytes):
245
- chunk = chunk.encode(self.charset)
246
- await send({"type": "http.response.body", "body": chunk, "more_body": True})
247
-
248
- await send({"type": "http.response.body", "body": b"", "more_body": False})
249
-
250
- async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
251
- async with anyio.create_task_group() as task_group:
252
-
253
- async def wrap(func: "typing.Callable[[], typing.Awaitable[None]]") -> None:
254
- await func()
255
- task_group.cancel_scope.cancel()
256
-
257
- task_group.start_soon(wrap, partial(self.stream_response, send))
258
- await wrap(partial(self.listen_for_disconnect, receive))
259
-
260
- if self.background is not None:
261
- await self.background()
262
-
263
-
264
- class FileResponse(Response):
265
- chunk_size = 64 * 1024
266
-
267
- def __init__(
268
- self,
269
- path: typing.Union[str, "os.PathLike[str]"],
270
- status_code: int = 200,
271
- headers: typing.Optional[typing.Mapping[str, str]] = None,
272
- media_type: typing.Optional[str] = None,
273
- background: typing.Optional[BackgroundTask] = None,
274
- filename: typing.Optional[str] = None,
275
- stat_result: typing.Optional[os.stat_result] = None,
276
- method: typing.Optional[str] = None,
277
- content_disposition_type: str = "attachment",
278
- ) -> None:
279
- self.path = path
280
- self.status_code = status_code
281
- self.filename = filename
282
- self.send_header_only = method is not None and method.upper() == "HEAD"
283
- if media_type is None:
284
- media_type = guess_type(filename or path)[0] or "text/plain"
285
- self.media_type = media_type
286
- self.background = background
287
- self.init_headers(headers)
288
- if self.filename is not None:
289
- content_disposition_filename = quote(self.filename)
290
- if content_disposition_filename != self.filename:
291
- content_disposition = "{}; filename*=utf-8''{}".format(
292
- content_disposition_type, content_disposition_filename
293
- )
294
- else:
295
- content_disposition = '{}; filename="{}"'.format(
296
- content_disposition_type, self.filename
297
- )
298
- self.headers.setdefault("content-disposition", content_disposition)
299
- self.stat_result = stat_result
300
- if stat_result is not None:
301
- self.set_stat_headers(stat_result)
302
-
303
- def set_stat_headers(self, stat_result: os.stat_result) -> None:
304
- content_length = str(stat_result.st_size)
305
- last_modified = formatdate(stat_result.st_mtime, usegmt=True)
306
- etag_base = str(stat_result.st_mtime) + "-" + str(stat_result.st_size)
307
- etag = md5_hexdigest(etag_base.encode(), usedforsecurity=False)
308
-
309
- self.headers.setdefault("content-length", content_length)
310
- self.headers.setdefault("last-modified", last_modified)
311
- self.headers.setdefault("etag", etag)
312
-
313
- async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
314
- if self.stat_result is None:
315
- try:
316
- stat_result = await anyio.to_thread.run_sync(os.stat, self.path)
317
- self.set_stat_headers(stat_result)
318
- except FileNotFoundError:
319
- raise RuntimeError(f"File at path {self.path} does not exist.")
320
- else:
321
- mode = stat_result.st_mode
322
- if not stat.S_ISREG(mode):
323
- raise RuntimeError(f"File at path {self.path} is not a file.")
324
- await send(
325
- {
326
- "type": "http.response.start",
327
- "status": self.status_code,
328
- "headers": self.raw_headers,
329
- }
330
- )
331
- if self.send_header_only:
332
- await send({"type": "http.response.body", "body": b"", "more_body": False})
333
- else:
334
- async with await anyio.open_file(self.path, mode="rb") as file:
335
- more_body = True
336
- while more_body:
337
- chunk = await file.read(self.chunk_size)
338
- more_body = len(chunk) == self.chunk_size
339
- await send(
340
- {
341
- "type": "http.response.body",
342
- "body": chunk,
343
- "more_body": more_body,
344
- }
345
- )
346
- if self.background is not None:
347
- await self.background()