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.
- prefect/__init__.py +8 -56
- prefect/_internal/compatibility/deprecated.py +6 -115
- prefect/_internal/compatibility/experimental.py +4 -79
- prefect/_internal/concurrency/api.py +0 -34
- prefect/_internal/concurrency/calls.py +0 -6
- prefect/_internal/concurrency/cancellation.py +0 -3
- prefect/_internal/concurrency/event_loop.py +0 -20
- prefect/_internal/concurrency/inspection.py +3 -3
- prefect/_internal/concurrency/threads.py +35 -0
- prefect/_internal/concurrency/waiters.py +0 -28
- prefect/_internal/pydantic/__init__.py +0 -45
- prefect/_internal/pydantic/v1_schema.py +21 -22
- prefect/_internal/pydantic/v2_schema.py +0 -2
- prefect/_internal/pydantic/v2_validated_func.py +18 -23
- prefect/_internal/schemas/bases.py +44 -177
- prefect/_internal/schemas/fields.py +1 -43
- prefect/_internal/schemas/validators.py +60 -158
- prefect/artifacts.py +161 -14
- prefect/automations.py +39 -4
- prefect/blocks/abstract.py +1 -1
- prefect/blocks/core.py +268 -148
- prefect/blocks/fields.py +2 -57
- prefect/blocks/kubernetes.py +8 -12
- prefect/blocks/notifications.py +40 -20
- prefect/blocks/system.py +22 -11
- prefect/blocks/webhook.py +2 -9
- prefect/client/base.py +4 -4
- prefect/client/cloud.py +8 -13
- prefect/client/orchestration.py +347 -341
- prefect/client/schemas/actions.py +92 -86
- prefect/client/schemas/filters.py +20 -40
- prefect/client/schemas/objects.py +147 -145
- prefect/client/schemas/responses.py +16 -24
- prefect/client/schemas/schedules.py +47 -35
- prefect/client/subscriptions.py +2 -2
- prefect/client/utilities.py +5 -2
- prefect/concurrency/asyncio.py +3 -1
- prefect/concurrency/events.py +1 -1
- prefect/concurrency/services.py +6 -3
- prefect/context.py +195 -27
- prefect/deployments/__init__.py +5 -6
- prefect/deployments/base.py +7 -5
- prefect/deployments/flow_runs.py +185 -0
- prefect/deployments/runner.py +50 -45
- prefect/deployments/schedules.py +28 -23
- prefect/deployments/steps/__init__.py +0 -1
- prefect/deployments/steps/core.py +1 -0
- prefect/deployments/steps/pull.py +7 -21
- prefect/engine.py +12 -2422
- prefect/events/actions.py +17 -23
- prefect/events/cli/automations.py +19 -6
- prefect/events/clients.py +14 -37
- prefect/events/filters.py +14 -18
- prefect/events/related.py +2 -2
- prefect/events/schemas/__init__.py +0 -5
- prefect/events/schemas/automations.py +55 -46
- prefect/events/schemas/deployment_triggers.py +7 -197
- prefect/events/schemas/events.py +34 -65
- prefect/events/schemas/labelling.py +10 -14
- prefect/events/utilities.py +2 -3
- prefect/events/worker.py +2 -3
- prefect/filesystems.py +6 -517
- prefect/{new_flow_engine.py → flow_engine.py} +313 -72
- prefect/flow_runs.py +377 -5
- prefect/flows.py +248 -165
- prefect/futures.py +186 -345
- prefect/infrastructure/__init__.py +0 -27
- prefect/infrastructure/provisioners/__init__.py +5 -3
- prefect/infrastructure/provisioners/cloud_run.py +11 -6
- prefect/infrastructure/provisioners/container_instance.py +11 -7
- prefect/infrastructure/provisioners/ecs.py +6 -4
- prefect/infrastructure/provisioners/modal.py +8 -5
- prefect/input/actions.py +2 -4
- prefect/input/run_input.py +5 -7
- prefect/logging/formatters.py +0 -2
- prefect/logging/handlers.py +3 -11
- prefect/logging/loggers.py +2 -2
- prefect/manifests.py +2 -1
- prefect/records/__init__.py +1 -0
- prefect/records/result_store.py +42 -0
- prefect/records/store.py +9 -0
- prefect/results.py +43 -39
- prefect/runner/runner.py +9 -9
- prefect/runner/server.py +6 -10
- prefect/runner/storage.py +3 -8
- prefect/runner/submit.py +2 -2
- prefect/runner/utils.py +2 -2
- prefect/serializers.py +24 -35
- prefect/server/api/collections_data/views/aggregate-worker-metadata.json +5 -14
- prefect/settings.py +70 -133
- prefect/states.py +17 -47
- prefect/task_engine.py +697 -58
- prefect/task_runners.py +269 -301
- prefect/task_server.py +53 -34
- prefect/tasks.py +327 -337
- prefect/transactions.py +220 -0
- prefect/types/__init__.py +61 -82
- prefect/utilities/asyncutils.py +195 -136
- prefect/utilities/callables.py +121 -41
- prefect/utilities/collections.py +23 -38
- prefect/utilities/dispatch.py +11 -3
- prefect/utilities/dockerutils.py +4 -0
- prefect/utilities/engine.py +140 -20
- prefect/utilities/importtools.py +26 -27
- prefect/utilities/pydantic.py +128 -38
- prefect/utilities/schema_tools/hydration.py +5 -1
- prefect/utilities/templating.py +12 -2
- prefect/variables.py +78 -61
- prefect/workers/__init__.py +0 -1
- prefect/workers/base.py +15 -17
- prefect/workers/process.py +3 -8
- prefect/workers/server.py +2 -2
- {prefect_client-2.19.3.dist-info → prefect_client-3.0.0rc1.dist-info}/METADATA +22 -21
- prefect_client-3.0.0rc1.dist-info/RECORD +176 -0
- prefect/_internal/pydantic/_base_model.py +0 -51
- prefect/_internal/pydantic/_compat.py +0 -82
- prefect/_internal/pydantic/_flags.py +0 -20
- prefect/_internal/pydantic/_types.py +0 -8
- prefect/_internal/pydantic/utilities/__init__.py +0 -0
- prefect/_internal/pydantic/utilities/config_dict.py +0 -72
- prefect/_internal/pydantic/utilities/field_validator.py +0 -150
- prefect/_internal/pydantic/utilities/model_construct.py +0 -56
- prefect/_internal/pydantic/utilities/model_copy.py +0 -55
- prefect/_internal/pydantic/utilities/model_dump.py +0 -136
- prefect/_internal/pydantic/utilities/model_dump_json.py +0 -112
- prefect/_internal/pydantic/utilities/model_fields.py +0 -50
- prefect/_internal/pydantic/utilities/model_fields_set.py +0 -29
- prefect/_internal/pydantic/utilities/model_json_schema.py +0 -82
- prefect/_internal/pydantic/utilities/model_rebuild.py +0 -80
- prefect/_internal/pydantic/utilities/model_validate.py +0 -75
- prefect/_internal/pydantic/utilities/model_validate_json.py +0 -68
- prefect/_internal/pydantic/utilities/model_validator.py +0 -87
- prefect/_internal/pydantic/utilities/type_adapter.py +0 -71
- prefect/_vendor/__init__.py +0 -0
- prefect/_vendor/fastapi/__init__.py +0 -25
- prefect/_vendor/fastapi/applications.py +0 -946
- prefect/_vendor/fastapi/background.py +0 -3
- prefect/_vendor/fastapi/concurrency.py +0 -44
- prefect/_vendor/fastapi/datastructures.py +0 -58
- prefect/_vendor/fastapi/dependencies/__init__.py +0 -0
- prefect/_vendor/fastapi/dependencies/models.py +0 -64
- prefect/_vendor/fastapi/dependencies/utils.py +0 -877
- prefect/_vendor/fastapi/encoders.py +0 -177
- prefect/_vendor/fastapi/exception_handlers.py +0 -40
- prefect/_vendor/fastapi/exceptions.py +0 -46
- prefect/_vendor/fastapi/logger.py +0 -3
- prefect/_vendor/fastapi/middleware/__init__.py +0 -1
- prefect/_vendor/fastapi/middleware/asyncexitstack.py +0 -25
- prefect/_vendor/fastapi/middleware/cors.py +0 -3
- prefect/_vendor/fastapi/middleware/gzip.py +0 -3
- prefect/_vendor/fastapi/middleware/httpsredirect.py +0 -3
- prefect/_vendor/fastapi/middleware/trustedhost.py +0 -3
- prefect/_vendor/fastapi/middleware/wsgi.py +0 -3
- prefect/_vendor/fastapi/openapi/__init__.py +0 -0
- prefect/_vendor/fastapi/openapi/constants.py +0 -2
- prefect/_vendor/fastapi/openapi/docs.py +0 -203
- prefect/_vendor/fastapi/openapi/models.py +0 -480
- prefect/_vendor/fastapi/openapi/utils.py +0 -485
- prefect/_vendor/fastapi/param_functions.py +0 -340
- prefect/_vendor/fastapi/params.py +0 -453
- prefect/_vendor/fastapi/requests.py +0 -4
- prefect/_vendor/fastapi/responses.py +0 -40
- prefect/_vendor/fastapi/routing.py +0 -1331
- prefect/_vendor/fastapi/security/__init__.py +0 -15
- prefect/_vendor/fastapi/security/api_key.py +0 -98
- prefect/_vendor/fastapi/security/base.py +0 -6
- prefect/_vendor/fastapi/security/http.py +0 -172
- prefect/_vendor/fastapi/security/oauth2.py +0 -227
- prefect/_vendor/fastapi/security/open_id_connect_url.py +0 -34
- prefect/_vendor/fastapi/security/utils.py +0 -10
- prefect/_vendor/fastapi/staticfiles.py +0 -1
- prefect/_vendor/fastapi/templating.py +0 -3
- prefect/_vendor/fastapi/testclient.py +0 -1
- prefect/_vendor/fastapi/types.py +0 -3
- prefect/_vendor/fastapi/utils.py +0 -235
- prefect/_vendor/fastapi/websockets.py +0 -7
- prefect/_vendor/starlette/__init__.py +0 -1
- prefect/_vendor/starlette/_compat.py +0 -28
- prefect/_vendor/starlette/_exception_handler.py +0 -80
- prefect/_vendor/starlette/_utils.py +0 -88
- prefect/_vendor/starlette/applications.py +0 -261
- prefect/_vendor/starlette/authentication.py +0 -159
- prefect/_vendor/starlette/background.py +0 -43
- prefect/_vendor/starlette/concurrency.py +0 -59
- prefect/_vendor/starlette/config.py +0 -151
- prefect/_vendor/starlette/convertors.py +0 -87
- prefect/_vendor/starlette/datastructures.py +0 -707
- prefect/_vendor/starlette/endpoints.py +0 -130
- prefect/_vendor/starlette/exceptions.py +0 -60
- prefect/_vendor/starlette/formparsers.py +0 -276
- prefect/_vendor/starlette/middleware/__init__.py +0 -17
- prefect/_vendor/starlette/middleware/authentication.py +0 -52
- prefect/_vendor/starlette/middleware/base.py +0 -220
- prefect/_vendor/starlette/middleware/cors.py +0 -176
- prefect/_vendor/starlette/middleware/errors.py +0 -265
- prefect/_vendor/starlette/middleware/exceptions.py +0 -74
- prefect/_vendor/starlette/middleware/gzip.py +0 -113
- prefect/_vendor/starlette/middleware/httpsredirect.py +0 -19
- prefect/_vendor/starlette/middleware/sessions.py +0 -82
- prefect/_vendor/starlette/middleware/trustedhost.py +0 -64
- prefect/_vendor/starlette/middleware/wsgi.py +0 -147
- prefect/_vendor/starlette/requests.py +0 -328
- prefect/_vendor/starlette/responses.py +0 -347
- prefect/_vendor/starlette/routing.py +0 -933
- prefect/_vendor/starlette/schemas.py +0 -154
- prefect/_vendor/starlette/staticfiles.py +0 -248
- prefect/_vendor/starlette/status.py +0 -199
- prefect/_vendor/starlette/templating.py +0 -231
- prefect/_vendor/starlette/testclient.py +0 -804
- prefect/_vendor/starlette/types.py +0 -30
- prefect/_vendor/starlette/websockets.py +0 -193
- prefect/agent.py +0 -698
- prefect/deployments/deployments.py +0 -1042
- prefect/deprecated/__init__.py +0 -0
- prefect/deprecated/data_documents.py +0 -350
- prefect/deprecated/packaging/__init__.py +0 -12
- prefect/deprecated/packaging/base.py +0 -96
- prefect/deprecated/packaging/docker.py +0 -146
- prefect/deprecated/packaging/file.py +0 -92
- prefect/deprecated/packaging/orion.py +0 -80
- prefect/deprecated/packaging/serializers.py +0 -171
- prefect/events/instrument.py +0 -135
- prefect/infrastructure/base.py +0 -323
- prefect/infrastructure/container.py +0 -818
- prefect/infrastructure/kubernetes.py +0 -920
- prefect/infrastructure/process.py +0 -289
- prefect/new_task_engine.py +0 -423
- prefect/pydantic/__init__.py +0 -76
- prefect/pydantic/main.py +0 -39
- prefect/software/__init__.py +0 -2
- prefect/software/base.py +0 -50
- prefect/software/conda.py +0 -199
- prefect/software/pip.py +0 -122
- prefect/software/python.py +0 -52
- prefect/workers/block.py +0 -218
- prefect_client-2.19.3.dist-info/RECORD +0 -292
- {prefect_client-2.19.3.dist-info → prefect_client-3.0.0rc1.dist-info}/LICENSE +0 -0
- {prefect_client-2.19.3.dist-info → prefect_client-3.0.0rc1.dist-info}/WHEEL +0 -0
- {prefect_client-2.19.3.dist-info → prefect_client-3.0.0rc1.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()
|