prefect-client 2.20.2__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.
- prefect/__init__.py +74 -110
- prefect/_internal/compatibility/deprecated.py +6 -115
- prefect/_internal/compatibility/experimental.py +4 -79
- prefect/_internal/compatibility/migration.py +166 -0
- prefect/_internal/concurrency/__init__.py +2 -2
- prefect/_internal/concurrency/api.py +1 -35
- 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/primitives.py +1 -0
- prefect/_internal/concurrency/services.py +23 -0
- prefect/_internal/concurrency/threads.py +35 -0
- prefect/_internal/concurrency/waiters.py +0 -28
- prefect/_internal/integrations.py +7 -0
- prefect/_internal/pydantic/__init__.py +0 -45
- prefect/_internal/pydantic/annotations/pendulum.py +2 -2
- 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/pytz.py +1 -1
- prefect/_internal/retries.py +61 -0
- prefect/_internal/schemas/bases.py +45 -177
- prefect/_internal/schemas/fields.py +1 -43
- prefect/_internal/schemas/validators.py +47 -233
- prefect/agent.py +3 -695
- prefect/artifacts.py +173 -14
- prefect/automations.py +39 -4
- prefect/blocks/abstract.py +1 -1
- prefect/blocks/core.py +423 -164
- prefect/blocks/fields.py +2 -57
- prefect/blocks/notifications.py +43 -28
- prefect/blocks/redis.py +168 -0
- prefect/blocks/system.py +67 -20
- prefect/blocks/webhook.py +2 -9
- prefect/cache_policies.py +239 -0
- prefect/client/__init__.py +4 -0
- prefect/client/base.py +33 -27
- prefect/client/cloud.py +65 -20
- prefect/client/collections.py +1 -1
- prefect/client/orchestration.py +667 -440
- prefect/client/schemas/actions.py +115 -100
- prefect/client/schemas/filters.py +46 -52
- prefect/client/schemas/objects.py +228 -178
- prefect/client/schemas/responses.py +18 -36
- prefect/client/schemas/schedules.py +55 -36
- prefect/client/schemas/sorting.py +2 -0
- prefect/client/subscriptions.py +8 -7
- prefect/client/types/flexible_schedule_list.py +11 -0
- prefect/client/utilities.py +9 -6
- prefect/concurrency/asyncio.py +60 -11
- prefect/concurrency/context.py +24 -0
- prefect/concurrency/events.py +2 -2
- prefect/concurrency/services.py +46 -16
- prefect/concurrency/sync.py +51 -7
- prefect/concurrency/v1/asyncio.py +143 -0
- prefect/concurrency/v1/context.py +27 -0
- prefect/concurrency/v1/events.py +61 -0
- prefect/concurrency/v1/services.py +116 -0
- prefect/concurrency/v1/sync.py +92 -0
- prefect/context.py +246 -149
- prefect/deployments/__init__.py +33 -18
- prefect/deployments/base.py +10 -15
- prefect/deployments/deployments.py +2 -1048
- prefect/deployments/flow_runs.py +178 -0
- prefect/deployments/runner.py +72 -173
- prefect/deployments/schedules.py +31 -25
- prefect/deployments/steps/__init__.py +0 -1
- prefect/deployments/steps/core.py +7 -0
- prefect/deployments/steps/pull.py +15 -21
- prefect/deployments/steps/utility.py +2 -1
- prefect/docker/__init__.py +20 -0
- prefect/docker/docker_image.py +82 -0
- prefect/engine.py +15 -2466
- prefect/events/actions.py +17 -23
- prefect/events/cli/automations.py +20 -7
- prefect/events/clients.py +142 -80
- prefect/events/filters.py +14 -18
- prefect/events/related.py +74 -75
- 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 +46 -65
- prefect/events/schemas/labelling.py +10 -14
- prefect/events/utilities.py +4 -5
- prefect/events/worker.py +23 -8
- prefect/exceptions.py +15 -0
- prefect/filesystems.py +30 -529
- prefect/flow_engine.py +827 -0
- prefect/flow_runs.py +379 -7
- prefect/flows.py +470 -360
- prefect/futures.py +382 -331
- prefect/infrastructure/__init__.py +5 -26
- prefect/infrastructure/base.py +3 -320
- prefect/infrastructure/provisioners/__init__.py +5 -3
- prefect/infrastructure/provisioners/cloud_run.py +13 -8
- prefect/infrastructure/provisioners/container_instance.py +14 -9
- prefect/infrastructure/provisioners/ecs.py +10 -8
- prefect/infrastructure/provisioners/modal.py +8 -5
- prefect/input/__init__.py +4 -0
- prefect/input/actions.py +2 -4
- prefect/input/run_input.py +9 -9
- prefect/logging/formatters.py +2 -4
- prefect/logging/handlers.py +9 -14
- prefect/logging/loggers.py +5 -5
- prefect/main.py +72 -0
- prefect/plugins.py +2 -64
- prefect/profiles.toml +16 -2
- prefect/records/__init__.py +1 -0
- prefect/records/base.py +223 -0
- prefect/records/filesystem.py +207 -0
- prefect/records/memory.py +178 -0
- prefect/records/result_store.py +64 -0
- prefect/results.py +577 -504
- prefect/runner/runner.py +124 -51
- prefect/runner/server.py +32 -34
- prefect/runner/storage.py +3 -12
- prefect/runner/submit.py +2 -10
- prefect/runner/utils.py +2 -2
- prefect/runtime/__init__.py +1 -0
- prefect/runtime/deployment.py +1 -0
- prefect/runtime/flow_run.py +40 -5
- prefect/runtime/task_run.py +1 -0
- prefect/serializers.py +28 -39
- prefect/server/api/collections_data/views/aggregate-worker-metadata.json +5 -14
- prefect/settings.py +209 -332
- prefect/states.py +160 -63
- prefect/task_engine.py +1478 -57
- prefect/task_runners.py +383 -287
- prefect/task_runs.py +240 -0
- prefect/task_worker.py +463 -0
- prefect/tasks.py +684 -374
- prefect/transactions.py +410 -0
- prefect/types/__init__.py +72 -86
- prefect/types/entrypoint.py +13 -0
- prefect/utilities/annotations.py +4 -3
- prefect/utilities/asyncutils.py +227 -148
- prefect/utilities/callables.py +138 -48
- prefect/utilities/collections.py +134 -86
- prefect/utilities/dispatch.py +27 -14
- prefect/utilities/dockerutils.py +11 -4
- prefect/utilities/engine.py +186 -32
- prefect/utilities/filesystem.py +4 -5
- prefect/utilities/importtools.py +26 -27
- prefect/utilities/pydantic.py +128 -38
- prefect/utilities/schema_tools/hydration.py +18 -1
- prefect/utilities/schema_tools/validation.py +30 -0
- prefect/utilities/services.py +35 -9
- prefect/utilities/templating.py +12 -2
- prefect/utilities/timeout.py +20 -5
- prefect/utilities/urls.py +195 -0
- prefect/utilities/visualization.py +1 -0
- prefect/variables.py +78 -59
- prefect/workers/__init__.py +0 -1
- prefect/workers/base.py +237 -244
- prefect/workers/block.py +5 -226
- prefect/workers/cloud.py +6 -0
- prefect/workers/process.py +265 -12
- prefect/workers/server.py +29 -11
- {prefect_client-2.20.2.dist-info → prefect_client-3.0.0.dist-info}/METADATA +30 -26
- prefect_client-3.0.0.dist-info/RECORD +201 -0
- {prefect_client-2.20.2.dist-info → prefect_client-3.0.0.dist-info}/WHEEL +1 -1
- 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/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/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/py.typed +0 -0
- 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/py.typed +0 -0
- 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/blocks/kubernetes.py +0 -119
- 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/container.py +0 -824
- prefect/infrastructure/kubernetes.py +0 -920
- prefect/infrastructure/process.py +0 -289
- prefect/manifests.py +0 -20
- prefect/new_flow_engine.py +0 -449
- 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/task_server.py +0 -322
- prefect_client-2.20.2.dist-info/RECORD +0 -294
- /prefect/{_internal/pydantic/utilities → client/types}/__init__.py +0 -0
- /prefect/{_vendor → concurrency/v1}/__init__.py +0 -0
- {prefect_client-2.20.2.dist-info → prefect_client-3.0.0.dist-info}/LICENSE +0 -0
- {prefect_client-2.20.2.dist-info → prefect_client-3.0.0.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)
|