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.
- 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 +405 -153
- 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 +650 -442
- 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 -2475
- 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 +117 -47
- 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 +137 -45
- 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.4.dist-info → prefect_client-3.0.0.dist-info}/METADATA +28 -24
- prefect_client-3.0.0.dist-info/RECORD +201 -0
- {prefect_client-2.20.4.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.4.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.4.dist-info → prefect_client-3.0.0.dist-info}/LICENSE +0 -0
- {prefect_client-2.20.4.dist-info → prefect_client-3.0.0.dist-info}/top_level.txt +0 -0
@@ -1,154 +0,0 @@
|
|
1
|
-
import inspect
|
2
|
-
import re
|
3
|
-
import typing
|
4
|
-
|
5
|
-
from prefect._vendor.starlette.requests import Request
|
6
|
-
from prefect._vendor.starlette.responses import Response
|
7
|
-
from prefect._vendor.starlette.routing import BaseRoute, Host, Mount, Route
|
8
|
-
|
9
|
-
try:
|
10
|
-
import yaml
|
11
|
-
except ModuleNotFoundError: # pragma: nocover
|
12
|
-
yaml = None # type: ignore[assignment]
|
13
|
-
|
14
|
-
|
15
|
-
class OpenAPIResponse(Response):
|
16
|
-
media_type = "application/vnd.oai.openapi"
|
17
|
-
|
18
|
-
def render(self, content: typing.Any) -> bytes:
|
19
|
-
assert yaml is not None, "`pyyaml` must be installed to use OpenAPIResponse."
|
20
|
-
assert isinstance(
|
21
|
-
content, dict
|
22
|
-
), "The schema passed to OpenAPIResponse should be a dictionary."
|
23
|
-
return yaml.dump(content, default_flow_style=False).encode("utf-8")
|
24
|
-
|
25
|
-
|
26
|
-
class EndpointInfo(typing.NamedTuple):
|
27
|
-
path: str
|
28
|
-
http_method: str
|
29
|
-
func: typing.Callable[..., typing.Any]
|
30
|
-
|
31
|
-
|
32
|
-
class BaseSchemaGenerator:
|
33
|
-
def get_schema(
|
34
|
-
self, routes: typing.List[BaseRoute]
|
35
|
-
) -> typing.Dict[str, typing.Any]:
|
36
|
-
raise NotImplementedError() # pragma: no cover
|
37
|
-
|
38
|
-
def get_endpoints(
|
39
|
-
self, routes: typing.List[BaseRoute]
|
40
|
-
) -> typing.List[EndpointInfo]:
|
41
|
-
"""
|
42
|
-
Given the routes, yields the following information:
|
43
|
-
|
44
|
-
- path
|
45
|
-
eg: /users/
|
46
|
-
- http_method
|
47
|
-
one of 'get', 'post', 'put', 'patch', 'delete', 'options'
|
48
|
-
- func
|
49
|
-
method ready to extract the docstring
|
50
|
-
"""
|
51
|
-
endpoints_info: typing.List[EndpointInfo] = []
|
52
|
-
|
53
|
-
for route in routes:
|
54
|
-
if isinstance(route, (Mount, Host)):
|
55
|
-
routes = route.routes or []
|
56
|
-
if isinstance(route, Mount):
|
57
|
-
path = self._remove_converter(route.path)
|
58
|
-
else:
|
59
|
-
path = ""
|
60
|
-
sub_endpoints = [
|
61
|
-
EndpointInfo(
|
62
|
-
path="".join((path, sub_endpoint.path)),
|
63
|
-
http_method=sub_endpoint.http_method,
|
64
|
-
func=sub_endpoint.func,
|
65
|
-
)
|
66
|
-
for sub_endpoint in self.get_endpoints(routes)
|
67
|
-
]
|
68
|
-
endpoints_info.extend(sub_endpoints)
|
69
|
-
|
70
|
-
elif not isinstance(route, Route) or not route.include_in_schema:
|
71
|
-
continue
|
72
|
-
|
73
|
-
elif inspect.isfunction(route.endpoint) or inspect.ismethod(route.endpoint):
|
74
|
-
path = self._remove_converter(route.path)
|
75
|
-
for method in route.methods or ["GET"]:
|
76
|
-
if method == "HEAD":
|
77
|
-
continue
|
78
|
-
endpoints_info.append(
|
79
|
-
EndpointInfo(path, method.lower(), route.endpoint)
|
80
|
-
)
|
81
|
-
else:
|
82
|
-
path = self._remove_converter(route.path)
|
83
|
-
for method in ["get", "post", "put", "patch", "delete", "options"]:
|
84
|
-
if not hasattr(route.endpoint, method):
|
85
|
-
continue
|
86
|
-
func = getattr(route.endpoint, method)
|
87
|
-
endpoints_info.append(EndpointInfo(path, method.lower(), func))
|
88
|
-
|
89
|
-
return endpoints_info
|
90
|
-
|
91
|
-
def _remove_converter(self, path: str) -> str:
|
92
|
-
"""
|
93
|
-
Remove the converter from the path.
|
94
|
-
For example, a route like this:
|
95
|
-
Route("/users/{id:int}", endpoint=get_user, methods=["GET"])
|
96
|
-
Should be represented as `/users/{id}` in the OpenAPI schema.
|
97
|
-
"""
|
98
|
-
return re.sub(r":\w+}", "}", path)
|
99
|
-
|
100
|
-
def parse_docstring(
|
101
|
-
self, func_or_method: typing.Callable[..., typing.Any]
|
102
|
-
) -> typing.Dict[str, typing.Any]:
|
103
|
-
"""
|
104
|
-
Given a function, parse the docstring as YAML and return a dictionary of info.
|
105
|
-
"""
|
106
|
-
docstring = func_or_method.__doc__
|
107
|
-
if not docstring:
|
108
|
-
return {}
|
109
|
-
|
110
|
-
assert yaml is not None, "`pyyaml` must be installed to use parse_docstring."
|
111
|
-
|
112
|
-
# We support having regular docstrings before the schema
|
113
|
-
# definition. Here we return just the schema part from
|
114
|
-
# the docstring.
|
115
|
-
docstring = docstring.split("---")[-1]
|
116
|
-
|
117
|
-
parsed = yaml.safe_load(docstring)
|
118
|
-
|
119
|
-
if not isinstance(parsed, dict):
|
120
|
-
# A regular docstring (not yaml formatted) can return
|
121
|
-
# a simple string here, which wouldn't follow the schema.
|
122
|
-
return {}
|
123
|
-
|
124
|
-
return parsed
|
125
|
-
|
126
|
-
def OpenAPIResponse(self, request: Request) -> Response:
|
127
|
-
routes = request.app.routes
|
128
|
-
schema = self.get_schema(routes=routes)
|
129
|
-
return OpenAPIResponse(schema)
|
130
|
-
|
131
|
-
|
132
|
-
class SchemaGenerator(BaseSchemaGenerator):
|
133
|
-
def __init__(self, base_schema: typing.Dict[str, typing.Any]) -> None:
|
134
|
-
self.base_schema = base_schema
|
135
|
-
|
136
|
-
def get_schema(
|
137
|
-
self, routes: typing.List[BaseRoute]
|
138
|
-
) -> typing.Dict[str, typing.Any]:
|
139
|
-
schema = dict(self.base_schema)
|
140
|
-
schema.setdefault("paths", {})
|
141
|
-
endpoints_info = self.get_endpoints(routes)
|
142
|
-
|
143
|
-
for endpoint in endpoints_info:
|
144
|
-
parsed = self.parse_docstring(endpoint.func)
|
145
|
-
|
146
|
-
if not parsed:
|
147
|
-
continue
|
148
|
-
|
149
|
-
if endpoint.path not in schema["paths"]:
|
150
|
-
schema["paths"][endpoint.path] = {}
|
151
|
-
|
152
|
-
schema["paths"][endpoint.path][endpoint.http_method] = parsed
|
153
|
-
|
154
|
-
return schema
|
@@ -1,248 +0,0 @@
|
|
1
|
-
import importlib.util
|
2
|
-
import os
|
3
|
-
import re
|
4
|
-
import stat
|
5
|
-
import typing
|
6
|
-
from email.utils import parsedate
|
7
|
-
|
8
|
-
import anyio
|
9
|
-
from prefect._vendor.starlette.datastructures import URL, Headers
|
10
|
-
from prefect._vendor.starlette.exceptions import HTTPException
|
11
|
-
from prefect._vendor.starlette.responses import FileResponse, RedirectResponse, Response
|
12
|
-
from prefect._vendor.starlette.types import Receive, Scope, Send
|
13
|
-
|
14
|
-
PathLike = typing.Union[str, "os.PathLike[str]"]
|
15
|
-
|
16
|
-
|
17
|
-
class NotModifiedResponse(Response):
|
18
|
-
NOT_MODIFIED_HEADERS = (
|
19
|
-
"cache-control",
|
20
|
-
"content-location",
|
21
|
-
"date",
|
22
|
-
"etag",
|
23
|
-
"expires",
|
24
|
-
"vary",
|
25
|
-
)
|
26
|
-
|
27
|
-
def __init__(self, headers: Headers):
|
28
|
-
super().__init__(
|
29
|
-
status_code=304,
|
30
|
-
headers={
|
31
|
-
name: value
|
32
|
-
for name, value in headers.items()
|
33
|
-
if name in self.NOT_MODIFIED_HEADERS
|
34
|
-
},
|
35
|
-
)
|
36
|
-
|
37
|
-
|
38
|
-
class StaticFiles:
|
39
|
-
def __init__(
|
40
|
-
self,
|
41
|
-
*,
|
42
|
-
directory: typing.Optional[PathLike] = None,
|
43
|
-
packages: typing.Optional[
|
44
|
-
typing.List[typing.Union[str, typing.Tuple[str, str]]]
|
45
|
-
] = None,
|
46
|
-
html: bool = False,
|
47
|
-
check_dir: bool = True,
|
48
|
-
follow_symlink: bool = False,
|
49
|
-
) -> None:
|
50
|
-
self.directory = directory
|
51
|
-
self.packages = packages
|
52
|
-
self.all_directories = self.get_directories(directory, packages)
|
53
|
-
self.html = html
|
54
|
-
self.config_checked = False
|
55
|
-
self.follow_symlink = follow_symlink
|
56
|
-
if check_dir and directory is not None and not os.path.isdir(directory):
|
57
|
-
raise RuntimeError(f"Directory '{directory}' does not exist")
|
58
|
-
|
59
|
-
def get_directories(
|
60
|
-
self,
|
61
|
-
directory: typing.Optional[PathLike] = None,
|
62
|
-
packages: typing.Optional[
|
63
|
-
typing.List[typing.Union[str, typing.Tuple[str, str]]]
|
64
|
-
] = None,
|
65
|
-
) -> typing.List[PathLike]:
|
66
|
-
"""
|
67
|
-
Given `directory` and `packages` arguments, return a list of all the
|
68
|
-
directories that should be used for serving static files from.
|
69
|
-
"""
|
70
|
-
directories = []
|
71
|
-
if directory is not None:
|
72
|
-
directories.append(directory)
|
73
|
-
|
74
|
-
for package in packages or []:
|
75
|
-
if isinstance(package, tuple):
|
76
|
-
package, statics_dir = package
|
77
|
-
else:
|
78
|
-
statics_dir = "statics"
|
79
|
-
spec = importlib.util.find_spec(package)
|
80
|
-
assert spec is not None, f"Package {package!r} could not be found."
|
81
|
-
assert spec.origin is not None, f"Package {package!r} could not be found."
|
82
|
-
package_directory = os.path.normpath(
|
83
|
-
os.path.join(spec.origin, "..", statics_dir)
|
84
|
-
)
|
85
|
-
assert os.path.isdir(
|
86
|
-
package_directory
|
87
|
-
), f"Directory '{statics_dir!r}' in package {package!r} could not be found."
|
88
|
-
directories.append(package_directory)
|
89
|
-
|
90
|
-
return directories
|
91
|
-
|
92
|
-
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
93
|
-
"""
|
94
|
-
The ASGI entry point.
|
95
|
-
"""
|
96
|
-
assert scope["type"] == "http"
|
97
|
-
|
98
|
-
if not self.config_checked:
|
99
|
-
await self.check_config()
|
100
|
-
self.config_checked = True
|
101
|
-
|
102
|
-
path = self.get_path(scope)
|
103
|
-
response = await self.get_response(path, scope)
|
104
|
-
await response(scope, receive, send)
|
105
|
-
|
106
|
-
def get_path(self, scope: Scope) -> str:
|
107
|
-
"""
|
108
|
-
Given the ASGI scope, return the `path` string to serve up,
|
109
|
-
with OS specific path separators, and any '..', '.' components removed.
|
110
|
-
"""
|
111
|
-
root_path = scope.get("route_root_path", scope.get("root_path", ""))
|
112
|
-
path = scope.get("route_path", re.sub(r"^" + root_path, "", scope["path"]))
|
113
|
-
return os.path.normpath(os.path.join(*path.split("/"))) # type: ignore[no-any-return] # noqa: E501
|
114
|
-
|
115
|
-
async def get_response(self, path: str, scope: Scope) -> Response:
|
116
|
-
"""
|
117
|
-
Returns an HTTP response, given the incoming path, method and request headers.
|
118
|
-
"""
|
119
|
-
if scope["method"] not in ("GET", "HEAD"):
|
120
|
-
raise HTTPException(status_code=405)
|
121
|
-
|
122
|
-
try:
|
123
|
-
full_path, stat_result = await anyio.to_thread.run_sync(
|
124
|
-
self.lookup_path, path
|
125
|
-
)
|
126
|
-
except PermissionError:
|
127
|
-
raise HTTPException(status_code=401)
|
128
|
-
except OSError:
|
129
|
-
raise
|
130
|
-
|
131
|
-
if stat_result and stat.S_ISREG(stat_result.st_mode):
|
132
|
-
# We have a static file to serve.
|
133
|
-
return self.file_response(full_path, stat_result, scope)
|
134
|
-
|
135
|
-
elif stat_result and stat.S_ISDIR(stat_result.st_mode) and self.html:
|
136
|
-
# We're in HTML mode, and have got a directory URL.
|
137
|
-
# Check if we have 'index.html' file to serve.
|
138
|
-
index_path = os.path.join(path, "index.html")
|
139
|
-
full_path, stat_result = await anyio.to_thread.run_sync(
|
140
|
-
self.lookup_path, index_path
|
141
|
-
)
|
142
|
-
if stat_result is not None and stat.S_ISREG(stat_result.st_mode):
|
143
|
-
if not scope["path"].endswith("/"):
|
144
|
-
# Directory URLs should redirect to always end in "/".
|
145
|
-
url = URL(scope=scope)
|
146
|
-
url = url.replace(path=url.path + "/")
|
147
|
-
return RedirectResponse(url=url)
|
148
|
-
return self.file_response(full_path, stat_result, scope)
|
149
|
-
|
150
|
-
if self.html:
|
151
|
-
# Check for '404.html' if we're in HTML mode.
|
152
|
-
full_path, stat_result = await anyio.to_thread.run_sync(
|
153
|
-
self.lookup_path, "404.html"
|
154
|
-
)
|
155
|
-
if stat_result and stat.S_ISREG(stat_result.st_mode):
|
156
|
-
return FileResponse(
|
157
|
-
full_path,
|
158
|
-
stat_result=stat_result,
|
159
|
-
method=scope["method"],
|
160
|
-
status_code=404,
|
161
|
-
)
|
162
|
-
raise HTTPException(status_code=404)
|
163
|
-
|
164
|
-
def lookup_path(
|
165
|
-
self, path: str
|
166
|
-
) -> typing.Tuple[str, typing.Optional[os.stat_result]]:
|
167
|
-
for directory in self.all_directories:
|
168
|
-
joined_path = os.path.join(directory, path)
|
169
|
-
if self.follow_symlink:
|
170
|
-
full_path = os.path.abspath(joined_path)
|
171
|
-
else:
|
172
|
-
full_path = os.path.realpath(joined_path)
|
173
|
-
directory = os.path.realpath(directory)
|
174
|
-
if os.path.commonpath([full_path, directory]) != directory:
|
175
|
-
# Don't allow misbehaving clients to break out of the static files
|
176
|
-
# directory.
|
177
|
-
continue
|
178
|
-
try:
|
179
|
-
return full_path, os.stat(full_path)
|
180
|
-
except (FileNotFoundError, NotADirectoryError):
|
181
|
-
continue
|
182
|
-
return "", None
|
183
|
-
|
184
|
-
def file_response(
|
185
|
-
self,
|
186
|
-
full_path: PathLike,
|
187
|
-
stat_result: os.stat_result,
|
188
|
-
scope: Scope,
|
189
|
-
status_code: int = 200,
|
190
|
-
) -> Response:
|
191
|
-
method = scope["method"]
|
192
|
-
request_headers = Headers(scope=scope)
|
193
|
-
|
194
|
-
response = FileResponse(
|
195
|
-
full_path, status_code=status_code, stat_result=stat_result, method=method
|
196
|
-
)
|
197
|
-
if self.is_not_modified(response.headers, request_headers):
|
198
|
-
return NotModifiedResponse(response.headers)
|
199
|
-
return response
|
200
|
-
|
201
|
-
async def check_config(self) -> None:
|
202
|
-
"""
|
203
|
-
Perform a one-off configuration check that StaticFiles is actually
|
204
|
-
pointed at a directory, so that we can raise loud errors rather than
|
205
|
-
just returning 404 responses.
|
206
|
-
"""
|
207
|
-
if self.directory is None:
|
208
|
-
return
|
209
|
-
|
210
|
-
try:
|
211
|
-
stat_result = await anyio.to_thread.run_sync(os.stat, self.directory)
|
212
|
-
except FileNotFoundError:
|
213
|
-
raise RuntimeError(
|
214
|
-
f"StaticFiles directory '{self.directory}' does not exist."
|
215
|
-
)
|
216
|
-
if not (stat.S_ISDIR(stat_result.st_mode) or stat.S_ISLNK(stat_result.st_mode)):
|
217
|
-
raise RuntimeError(
|
218
|
-
f"StaticFiles path '{self.directory}' is not a directory."
|
219
|
-
)
|
220
|
-
|
221
|
-
def is_not_modified(
|
222
|
-
self, response_headers: Headers, request_headers: Headers
|
223
|
-
) -> bool:
|
224
|
-
"""
|
225
|
-
Given the request and response headers, return `True` if an HTTP
|
226
|
-
"Not Modified" response could be returned instead.
|
227
|
-
"""
|
228
|
-
try:
|
229
|
-
if_none_match = request_headers["if-none-match"]
|
230
|
-
etag = response_headers["etag"]
|
231
|
-
if if_none_match == etag:
|
232
|
-
return True
|
233
|
-
except KeyError:
|
234
|
-
pass
|
235
|
-
|
236
|
-
try:
|
237
|
-
if_modified_since = parsedate(request_headers["if-modified-since"])
|
238
|
-
last_modified = parsedate(response_headers["last-modified"])
|
239
|
-
if (
|
240
|
-
if_modified_since is not None
|
241
|
-
and last_modified is not None
|
242
|
-
and if_modified_since >= last_modified
|
243
|
-
):
|
244
|
-
return True
|
245
|
-
except KeyError:
|
246
|
-
pass
|
247
|
-
|
248
|
-
return False
|
@@ -1,199 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
HTTP codes
|
3
|
-
See HTTP Status Code Registry:
|
4
|
-
https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
|
5
|
-
|
6
|
-
And RFC 2324 - https://tools.ietf.org/html/rfc2324
|
7
|
-
"""
|
8
|
-
import warnings
|
9
|
-
from typing import List
|
10
|
-
|
11
|
-
__all__ = (
|
12
|
-
"HTTP_100_CONTINUE",
|
13
|
-
"HTTP_101_SWITCHING_PROTOCOLS",
|
14
|
-
"HTTP_102_PROCESSING",
|
15
|
-
"HTTP_103_EARLY_HINTS",
|
16
|
-
"HTTP_200_OK",
|
17
|
-
"HTTP_201_CREATED",
|
18
|
-
"HTTP_202_ACCEPTED",
|
19
|
-
"HTTP_203_NON_AUTHORITATIVE_INFORMATION",
|
20
|
-
"HTTP_204_NO_CONTENT",
|
21
|
-
"HTTP_205_RESET_CONTENT",
|
22
|
-
"HTTP_206_PARTIAL_CONTENT",
|
23
|
-
"HTTP_207_MULTI_STATUS",
|
24
|
-
"HTTP_208_ALREADY_REPORTED",
|
25
|
-
"HTTP_226_IM_USED",
|
26
|
-
"HTTP_300_MULTIPLE_CHOICES",
|
27
|
-
"HTTP_301_MOVED_PERMANENTLY",
|
28
|
-
"HTTP_302_FOUND",
|
29
|
-
"HTTP_303_SEE_OTHER",
|
30
|
-
"HTTP_304_NOT_MODIFIED",
|
31
|
-
"HTTP_305_USE_PROXY",
|
32
|
-
"HTTP_306_RESERVED",
|
33
|
-
"HTTP_307_TEMPORARY_REDIRECT",
|
34
|
-
"HTTP_308_PERMANENT_REDIRECT",
|
35
|
-
"HTTP_400_BAD_REQUEST",
|
36
|
-
"HTTP_401_UNAUTHORIZED",
|
37
|
-
"HTTP_402_PAYMENT_REQUIRED",
|
38
|
-
"HTTP_403_FORBIDDEN",
|
39
|
-
"HTTP_404_NOT_FOUND",
|
40
|
-
"HTTP_405_METHOD_NOT_ALLOWED",
|
41
|
-
"HTTP_406_NOT_ACCEPTABLE",
|
42
|
-
"HTTP_407_PROXY_AUTHENTICATION_REQUIRED",
|
43
|
-
"HTTP_408_REQUEST_TIMEOUT",
|
44
|
-
"HTTP_409_CONFLICT",
|
45
|
-
"HTTP_410_GONE",
|
46
|
-
"HTTP_411_LENGTH_REQUIRED",
|
47
|
-
"HTTP_412_PRECONDITION_FAILED",
|
48
|
-
"HTTP_413_REQUEST_ENTITY_TOO_LARGE",
|
49
|
-
"HTTP_414_REQUEST_URI_TOO_LONG",
|
50
|
-
"HTTP_415_UNSUPPORTED_MEDIA_TYPE",
|
51
|
-
"HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE",
|
52
|
-
"HTTP_417_EXPECTATION_FAILED",
|
53
|
-
"HTTP_418_IM_A_TEAPOT",
|
54
|
-
"HTTP_421_MISDIRECTED_REQUEST",
|
55
|
-
"HTTP_422_UNPROCESSABLE_ENTITY",
|
56
|
-
"HTTP_423_LOCKED",
|
57
|
-
"HTTP_424_FAILED_DEPENDENCY",
|
58
|
-
"HTTP_425_TOO_EARLY",
|
59
|
-
"HTTP_426_UPGRADE_REQUIRED",
|
60
|
-
"HTTP_428_PRECONDITION_REQUIRED",
|
61
|
-
"HTTP_429_TOO_MANY_REQUESTS",
|
62
|
-
"HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE",
|
63
|
-
"HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS",
|
64
|
-
"HTTP_500_INTERNAL_SERVER_ERROR",
|
65
|
-
"HTTP_501_NOT_IMPLEMENTED",
|
66
|
-
"HTTP_502_BAD_GATEWAY",
|
67
|
-
"HTTP_503_SERVICE_UNAVAILABLE",
|
68
|
-
"HTTP_504_GATEWAY_TIMEOUT",
|
69
|
-
"HTTP_505_HTTP_VERSION_NOT_SUPPORTED",
|
70
|
-
"HTTP_506_VARIANT_ALSO_NEGOTIATES",
|
71
|
-
"HTTP_507_INSUFFICIENT_STORAGE",
|
72
|
-
"HTTP_508_LOOP_DETECTED",
|
73
|
-
"HTTP_510_NOT_EXTENDED",
|
74
|
-
"HTTP_511_NETWORK_AUTHENTICATION_REQUIRED",
|
75
|
-
"WS_1000_NORMAL_CLOSURE",
|
76
|
-
"WS_1001_GOING_AWAY",
|
77
|
-
"WS_1002_PROTOCOL_ERROR",
|
78
|
-
"WS_1003_UNSUPPORTED_DATA",
|
79
|
-
"WS_1005_NO_STATUS_RCVD",
|
80
|
-
"WS_1006_ABNORMAL_CLOSURE",
|
81
|
-
"WS_1007_INVALID_FRAME_PAYLOAD_DATA",
|
82
|
-
"WS_1008_POLICY_VIOLATION",
|
83
|
-
"WS_1009_MESSAGE_TOO_BIG",
|
84
|
-
"WS_1010_MANDATORY_EXT",
|
85
|
-
"WS_1011_INTERNAL_ERROR",
|
86
|
-
"WS_1012_SERVICE_RESTART",
|
87
|
-
"WS_1013_TRY_AGAIN_LATER",
|
88
|
-
"WS_1014_BAD_GATEWAY",
|
89
|
-
"WS_1015_TLS_HANDSHAKE",
|
90
|
-
)
|
91
|
-
|
92
|
-
HTTP_100_CONTINUE = 100
|
93
|
-
HTTP_101_SWITCHING_PROTOCOLS = 101
|
94
|
-
HTTP_102_PROCESSING = 102
|
95
|
-
HTTP_103_EARLY_HINTS = 103
|
96
|
-
HTTP_200_OK = 200
|
97
|
-
HTTP_201_CREATED = 201
|
98
|
-
HTTP_202_ACCEPTED = 202
|
99
|
-
HTTP_203_NON_AUTHORITATIVE_INFORMATION = 203
|
100
|
-
HTTP_204_NO_CONTENT = 204
|
101
|
-
HTTP_205_RESET_CONTENT = 205
|
102
|
-
HTTP_206_PARTIAL_CONTENT = 206
|
103
|
-
HTTP_207_MULTI_STATUS = 207
|
104
|
-
HTTP_208_ALREADY_REPORTED = 208
|
105
|
-
HTTP_226_IM_USED = 226
|
106
|
-
HTTP_300_MULTIPLE_CHOICES = 300
|
107
|
-
HTTP_301_MOVED_PERMANENTLY = 301
|
108
|
-
HTTP_302_FOUND = 302
|
109
|
-
HTTP_303_SEE_OTHER = 303
|
110
|
-
HTTP_304_NOT_MODIFIED = 304
|
111
|
-
HTTP_305_USE_PROXY = 305
|
112
|
-
HTTP_306_RESERVED = 306
|
113
|
-
HTTP_307_TEMPORARY_REDIRECT = 307
|
114
|
-
HTTP_308_PERMANENT_REDIRECT = 308
|
115
|
-
HTTP_400_BAD_REQUEST = 400
|
116
|
-
HTTP_401_UNAUTHORIZED = 401
|
117
|
-
HTTP_402_PAYMENT_REQUIRED = 402
|
118
|
-
HTTP_403_FORBIDDEN = 403
|
119
|
-
HTTP_404_NOT_FOUND = 404
|
120
|
-
HTTP_405_METHOD_NOT_ALLOWED = 405
|
121
|
-
HTTP_406_NOT_ACCEPTABLE = 406
|
122
|
-
HTTP_407_PROXY_AUTHENTICATION_REQUIRED = 407
|
123
|
-
HTTP_408_REQUEST_TIMEOUT = 408
|
124
|
-
HTTP_409_CONFLICT = 409
|
125
|
-
HTTP_410_GONE = 410
|
126
|
-
HTTP_411_LENGTH_REQUIRED = 411
|
127
|
-
HTTP_412_PRECONDITION_FAILED = 412
|
128
|
-
HTTP_413_REQUEST_ENTITY_TOO_LARGE = 413
|
129
|
-
HTTP_414_REQUEST_URI_TOO_LONG = 414
|
130
|
-
HTTP_415_UNSUPPORTED_MEDIA_TYPE = 415
|
131
|
-
HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE = 416
|
132
|
-
HTTP_417_EXPECTATION_FAILED = 417
|
133
|
-
HTTP_418_IM_A_TEAPOT = 418
|
134
|
-
HTTP_421_MISDIRECTED_REQUEST = 421
|
135
|
-
HTTP_422_UNPROCESSABLE_ENTITY = 422
|
136
|
-
HTTP_423_LOCKED = 423
|
137
|
-
HTTP_424_FAILED_DEPENDENCY = 424
|
138
|
-
HTTP_425_TOO_EARLY = 425
|
139
|
-
HTTP_426_UPGRADE_REQUIRED = 426
|
140
|
-
HTTP_428_PRECONDITION_REQUIRED = 428
|
141
|
-
HTTP_429_TOO_MANY_REQUESTS = 429
|
142
|
-
HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 431
|
143
|
-
HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS = 451
|
144
|
-
HTTP_500_INTERNAL_SERVER_ERROR = 500
|
145
|
-
HTTP_501_NOT_IMPLEMENTED = 501
|
146
|
-
HTTP_502_BAD_GATEWAY = 502
|
147
|
-
HTTP_503_SERVICE_UNAVAILABLE = 503
|
148
|
-
HTTP_504_GATEWAY_TIMEOUT = 504
|
149
|
-
HTTP_505_HTTP_VERSION_NOT_SUPPORTED = 505
|
150
|
-
HTTP_506_VARIANT_ALSO_NEGOTIATES = 506
|
151
|
-
HTTP_507_INSUFFICIENT_STORAGE = 507
|
152
|
-
HTTP_508_LOOP_DETECTED = 508
|
153
|
-
HTTP_510_NOT_EXTENDED = 510
|
154
|
-
HTTP_511_NETWORK_AUTHENTICATION_REQUIRED = 511
|
155
|
-
|
156
|
-
|
157
|
-
"""
|
158
|
-
WebSocket codes
|
159
|
-
https://www.iana.org/assignments/websocket/websocket.xml#close-code-number
|
160
|
-
https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent
|
161
|
-
"""
|
162
|
-
WS_1000_NORMAL_CLOSURE = 1000
|
163
|
-
WS_1001_GOING_AWAY = 1001
|
164
|
-
WS_1002_PROTOCOL_ERROR = 1002
|
165
|
-
WS_1003_UNSUPPORTED_DATA = 1003
|
166
|
-
WS_1005_NO_STATUS_RCVD = 1005
|
167
|
-
WS_1006_ABNORMAL_CLOSURE = 1006
|
168
|
-
WS_1007_INVALID_FRAME_PAYLOAD_DATA = 1007
|
169
|
-
WS_1008_POLICY_VIOLATION = 1008
|
170
|
-
WS_1009_MESSAGE_TOO_BIG = 1009
|
171
|
-
WS_1010_MANDATORY_EXT = 1010
|
172
|
-
WS_1011_INTERNAL_ERROR = 1011
|
173
|
-
WS_1012_SERVICE_RESTART = 1012
|
174
|
-
WS_1013_TRY_AGAIN_LATER = 1013
|
175
|
-
WS_1014_BAD_GATEWAY = 1014
|
176
|
-
WS_1015_TLS_HANDSHAKE = 1015
|
177
|
-
|
178
|
-
|
179
|
-
__deprecated__ = {"WS_1004_NO_STATUS_RCVD": 1004, "WS_1005_ABNORMAL_CLOSURE": 1005}
|
180
|
-
|
181
|
-
|
182
|
-
def __getattr__(name: str) -> int:
|
183
|
-
deprecation_changes = {
|
184
|
-
"WS_1004_NO_STATUS_RCVD": "WS_1005_NO_STATUS_RCVD",
|
185
|
-
"WS_1005_ABNORMAL_CLOSURE": "WS_1006_ABNORMAL_CLOSURE",
|
186
|
-
}
|
187
|
-
deprecated = __deprecated__.get(name)
|
188
|
-
if deprecated:
|
189
|
-
warnings.warn(
|
190
|
-
f"'{name}' is deprecated. Use '{deprecation_changes[name]}' instead.",
|
191
|
-
category=DeprecationWarning,
|
192
|
-
stacklevel=3,
|
193
|
-
)
|
194
|
-
return deprecated
|
195
|
-
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
|
196
|
-
|
197
|
-
|
198
|
-
def __dir__() -> List[str]:
|
199
|
-
return sorted(list(__all__) + list(__deprecated__.keys())) # pragma: no cover
|