prefect-client 2.20.4__py3-none-any.whl → 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (288) hide show
  1. prefect/__init__.py +74 -110
  2. prefect/_internal/compatibility/deprecated.py +6 -115
  3. prefect/_internal/compatibility/experimental.py +4 -79
  4. prefect/_internal/compatibility/migration.py +166 -0
  5. prefect/_internal/concurrency/__init__.py +2 -2
  6. prefect/_internal/concurrency/api.py +1 -35
  7. prefect/_internal/concurrency/calls.py +0 -6
  8. prefect/_internal/concurrency/cancellation.py +0 -3
  9. prefect/_internal/concurrency/event_loop.py +0 -20
  10. prefect/_internal/concurrency/inspection.py +3 -3
  11. prefect/_internal/concurrency/primitives.py +1 -0
  12. prefect/_internal/concurrency/services.py +23 -0
  13. prefect/_internal/concurrency/threads.py +35 -0
  14. prefect/_internal/concurrency/waiters.py +0 -28
  15. prefect/_internal/integrations.py +7 -0
  16. prefect/_internal/pydantic/__init__.py +0 -45
  17. prefect/_internal/pydantic/annotations/pendulum.py +2 -2
  18. prefect/_internal/pydantic/v1_schema.py +21 -22
  19. prefect/_internal/pydantic/v2_schema.py +0 -2
  20. prefect/_internal/pydantic/v2_validated_func.py +18 -23
  21. prefect/_internal/pytz.py +1 -1
  22. prefect/_internal/retries.py +61 -0
  23. prefect/_internal/schemas/bases.py +45 -177
  24. prefect/_internal/schemas/fields.py +1 -43
  25. prefect/_internal/schemas/validators.py +47 -233
  26. prefect/agent.py +3 -695
  27. prefect/artifacts.py +173 -14
  28. prefect/automations.py +39 -4
  29. prefect/blocks/abstract.py +1 -1
  30. prefect/blocks/core.py +405 -153
  31. prefect/blocks/fields.py +2 -57
  32. prefect/blocks/notifications.py +43 -28
  33. prefect/blocks/redis.py +168 -0
  34. prefect/blocks/system.py +67 -20
  35. prefect/blocks/webhook.py +2 -9
  36. prefect/cache_policies.py +239 -0
  37. prefect/client/__init__.py +4 -0
  38. prefect/client/base.py +33 -27
  39. prefect/client/cloud.py +65 -20
  40. prefect/client/collections.py +1 -1
  41. prefect/client/orchestration.py +650 -442
  42. prefect/client/schemas/actions.py +115 -100
  43. prefect/client/schemas/filters.py +46 -52
  44. prefect/client/schemas/objects.py +228 -178
  45. prefect/client/schemas/responses.py +18 -36
  46. prefect/client/schemas/schedules.py +55 -36
  47. prefect/client/schemas/sorting.py +2 -0
  48. prefect/client/subscriptions.py +8 -7
  49. prefect/client/types/flexible_schedule_list.py +11 -0
  50. prefect/client/utilities.py +9 -6
  51. prefect/concurrency/asyncio.py +60 -11
  52. prefect/concurrency/context.py +24 -0
  53. prefect/concurrency/events.py +2 -2
  54. prefect/concurrency/services.py +46 -16
  55. prefect/concurrency/sync.py +51 -7
  56. prefect/concurrency/v1/asyncio.py +143 -0
  57. prefect/concurrency/v1/context.py +27 -0
  58. prefect/concurrency/v1/events.py +61 -0
  59. prefect/concurrency/v1/services.py +116 -0
  60. prefect/concurrency/v1/sync.py +92 -0
  61. prefect/context.py +246 -149
  62. prefect/deployments/__init__.py +33 -18
  63. prefect/deployments/base.py +10 -15
  64. prefect/deployments/deployments.py +2 -1048
  65. prefect/deployments/flow_runs.py +178 -0
  66. prefect/deployments/runner.py +72 -173
  67. prefect/deployments/schedules.py +31 -25
  68. prefect/deployments/steps/__init__.py +0 -1
  69. prefect/deployments/steps/core.py +7 -0
  70. prefect/deployments/steps/pull.py +15 -21
  71. prefect/deployments/steps/utility.py +2 -1
  72. prefect/docker/__init__.py +20 -0
  73. prefect/docker/docker_image.py +82 -0
  74. prefect/engine.py +15 -2475
  75. prefect/events/actions.py +17 -23
  76. prefect/events/cli/automations.py +20 -7
  77. prefect/events/clients.py +142 -80
  78. prefect/events/filters.py +14 -18
  79. prefect/events/related.py +74 -75
  80. prefect/events/schemas/__init__.py +0 -5
  81. prefect/events/schemas/automations.py +55 -46
  82. prefect/events/schemas/deployment_triggers.py +7 -197
  83. prefect/events/schemas/events.py +46 -65
  84. prefect/events/schemas/labelling.py +10 -14
  85. prefect/events/utilities.py +4 -5
  86. prefect/events/worker.py +23 -8
  87. prefect/exceptions.py +15 -0
  88. prefect/filesystems.py +30 -529
  89. prefect/flow_engine.py +827 -0
  90. prefect/flow_runs.py +379 -7
  91. prefect/flows.py +470 -360
  92. prefect/futures.py +382 -331
  93. prefect/infrastructure/__init__.py +5 -26
  94. prefect/infrastructure/base.py +3 -320
  95. prefect/infrastructure/provisioners/__init__.py +5 -3
  96. prefect/infrastructure/provisioners/cloud_run.py +13 -8
  97. prefect/infrastructure/provisioners/container_instance.py +14 -9
  98. prefect/infrastructure/provisioners/ecs.py +10 -8
  99. prefect/infrastructure/provisioners/modal.py +8 -5
  100. prefect/input/__init__.py +4 -0
  101. prefect/input/actions.py +2 -4
  102. prefect/input/run_input.py +9 -9
  103. prefect/logging/formatters.py +2 -4
  104. prefect/logging/handlers.py +9 -14
  105. prefect/logging/loggers.py +5 -5
  106. prefect/main.py +72 -0
  107. prefect/plugins.py +2 -64
  108. prefect/profiles.toml +16 -2
  109. prefect/records/__init__.py +1 -0
  110. prefect/records/base.py +223 -0
  111. prefect/records/filesystem.py +207 -0
  112. prefect/records/memory.py +178 -0
  113. prefect/records/result_store.py +64 -0
  114. prefect/results.py +577 -504
  115. prefect/runner/runner.py +117 -47
  116. prefect/runner/server.py +32 -34
  117. prefect/runner/storage.py +3 -12
  118. prefect/runner/submit.py +2 -10
  119. prefect/runner/utils.py +2 -2
  120. prefect/runtime/__init__.py +1 -0
  121. prefect/runtime/deployment.py +1 -0
  122. prefect/runtime/flow_run.py +40 -5
  123. prefect/runtime/task_run.py +1 -0
  124. prefect/serializers.py +28 -39
  125. prefect/server/api/collections_data/views/aggregate-worker-metadata.json +5 -14
  126. prefect/settings.py +209 -332
  127. prefect/states.py +160 -63
  128. prefect/task_engine.py +1478 -57
  129. prefect/task_runners.py +383 -287
  130. prefect/task_runs.py +240 -0
  131. prefect/task_worker.py +463 -0
  132. prefect/tasks.py +684 -374
  133. prefect/transactions.py +410 -0
  134. prefect/types/__init__.py +72 -86
  135. prefect/types/entrypoint.py +13 -0
  136. prefect/utilities/annotations.py +4 -3
  137. prefect/utilities/asyncutils.py +227 -148
  138. prefect/utilities/callables.py +137 -45
  139. prefect/utilities/collections.py +134 -86
  140. prefect/utilities/dispatch.py +27 -14
  141. prefect/utilities/dockerutils.py +11 -4
  142. prefect/utilities/engine.py +186 -32
  143. prefect/utilities/filesystem.py +4 -5
  144. prefect/utilities/importtools.py +26 -27
  145. prefect/utilities/pydantic.py +128 -38
  146. prefect/utilities/schema_tools/hydration.py +18 -1
  147. prefect/utilities/schema_tools/validation.py +30 -0
  148. prefect/utilities/services.py +35 -9
  149. prefect/utilities/templating.py +12 -2
  150. prefect/utilities/timeout.py +20 -5
  151. prefect/utilities/urls.py +195 -0
  152. prefect/utilities/visualization.py +1 -0
  153. prefect/variables.py +78 -59
  154. prefect/workers/__init__.py +0 -1
  155. prefect/workers/base.py +237 -244
  156. prefect/workers/block.py +5 -226
  157. prefect/workers/cloud.py +6 -0
  158. prefect/workers/process.py +265 -12
  159. prefect/workers/server.py +29 -11
  160. {prefect_client-2.20.4.dist-info → prefect_client-3.0.0.dist-info}/METADATA +28 -24
  161. prefect_client-3.0.0.dist-info/RECORD +201 -0
  162. {prefect_client-2.20.4.dist-info → prefect_client-3.0.0.dist-info}/WHEEL +1 -1
  163. prefect/_internal/pydantic/_base_model.py +0 -51
  164. prefect/_internal/pydantic/_compat.py +0 -82
  165. prefect/_internal/pydantic/_flags.py +0 -20
  166. prefect/_internal/pydantic/_types.py +0 -8
  167. prefect/_internal/pydantic/utilities/config_dict.py +0 -72
  168. prefect/_internal/pydantic/utilities/field_validator.py +0 -150
  169. prefect/_internal/pydantic/utilities/model_construct.py +0 -56
  170. prefect/_internal/pydantic/utilities/model_copy.py +0 -55
  171. prefect/_internal/pydantic/utilities/model_dump.py +0 -136
  172. prefect/_internal/pydantic/utilities/model_dump_json.py +0 -112
  173. prefect/_internal/pydantic/utilities/model_fields.py +0 -50
  174. prefect/_internal/pydantic/utilities/model_fields_set.py +0 -29
  175. prefect/_internal/pydantic/utilities/model_json_schema.py +0 -82
  176. prefect/_internal/pydantic/utilities/model_rebuild.py +0 -80
  177. prefect/_internal/pydantic/utilities/model_validate.py +0 -75
  178. prefect/_internal/pydantic/utilities/model_validate_json.py +0 -68
  179. prefect/_internal/pydantic/utilities/model_validator.py +0 -87
  180. prefect/_internal/pydantic/utilities/type_adapter.py +0 -71
  181. prefect/_vendor/fastapi/__init__.py +0 -25
  182. prefect/_vendor/fastapi/applications.py +0 -946
  183. prefect/_vendor/fastapi/background.py +0 -3
  184. prefect/_vendor/fastapi/concurrency.py +0 -44
  185. prefect/_vendor/fastapi/datastructures.py +0 -58
  186. prefect/_vendor/fastapi/dependencies/__init__.py +0 -0
  187. prefect/_vendor/fastapi/dependencies/models.py +0 -64
  188. prefect/_vendor/fastapi/dependencies/utils.py +0 -877
  189. prefect/_vendor/fastapi/encoders.py +0 -177
  190. prefect/_vendor/fastapi/exception_handlers.py +0 -40
  191. prefect/_vendor/fastapi/exceptions.py +0 -46
  192. prefect/_vendor/fastapi/logger.py +0 -3
  193. prefect/_vendor/fastapi/middleware/__init__.py +0 -1
  194. prefect/_vendor/fastapi/middleware/asyncexitstack.py +0 -25
  195. prefect/_vendor/fastapi/middleware/cors.py +0 -3
  196. prefect/_vendor/fastapi/middleware/gzip.py +0 -3
  197. prefect/_vendor/fastapi/middleware/httpsredirect.py +0 -3
  198. prefect/_vendor/fastapi/middleware/trustedhost.py +0 -3
  199. prefect/_vendor/fastapi/middleware/wsgi.py +0 -3
  200. prefect/_vendor/fastapi/openapi/__init__.py +0 -0
  201. prefect/_vendor/fastapi/openapi/constants.py +0 -2
  202. prefect/_vendor/fastapi/openapi/docs.py +0 -203
  203. prefect/_vendor/fastapi/openapi/models.py +0 -480
  204. prefect/_vendor/fastapi/openapi/utils.py +0 -485
  205. prefect/_vendor/fastapi/param_functions.py +0 -340
  206. prefect/_vendor/fastapi/params.py +0 -453
  207. prefect/_vendor/fastapi/py.typed +0 -0
  208. prefect/_vendor/fastapi/requests.py +0 -4
  209. prefect/_vendor/fastapi/responses.py +0 -40
  210. prefect/_vendor/fastapi/routing.py +0 -1331
  211. prefect/_vendor/fastapi/security/__init__.py +0 -15
  212. prefect/_vendor/fastapi/security/api_key.py +0 -98
  213. prefect/_vendor/fastapi/security/base.py +0 -6
  214. prefect/_vendor/fastapi/security/http.py +0 -172
  215. prefect/_vendor/fastapi/security/oauth2.py +0 -227
  216. prefect/_vendor/fastapi/security/open_id_connect_url.py +0 -34
  217. prefect/_vendor/fastapi/security/utils.py +0 -10
  218. prefect/_vendor/fastapi/staticfiles.py +0 -1
  219. prefect/_vendor/fastapi/templating.py +0 -3
  220. prefect/_vendor/fastapi/testclient.py +0 -1
  221. prefect/_vendor/fastapi/types.py +0 -3
  222. prefect/_vendor/fastapi/utils.py +0 -235
  223. prefect/_vendor/fastapi/websockets.py +0 -7
  224. prefect/_vendor/starlette/__init__.py +0 -1
  225. prefect/_vendor/starlette/_compat.py +0 -28
  226. prefect/_vendor/starlette/_exception_handler.py +0 -80
  227. prefect/_vendor/starlette/_utils.py +0 -88
  228. prefect/_vendor/starlette/applications.py +0 -261
  229. prefect/_vendor/starlette/authentication.py +0 -159
  230. prefect/_vendor/starlette/background.py +0 -43
  231. prefect/_vendor/starlette/concurrency.py +0 -59
  232. prefect/_vendor/starlette/config.py +0 -151
  233. prefect/_vendor/starlette/convertors.py +0 -87
  234. prefect/_vendor/starlette/datastructures.py +0 -707
  235. prefect/_vendor/starlette/endpoints.py +0 -130
  236. prefect/_vendor/starlette/exceptions.py +0 -60
  237. prefect/_vendor/starlette/formparsers.py +0 -276
  238. prefect/_vendor/starlette/middleware/__init__.py +0 -17
  239. prefect/_vendor/starlette/middleware/authentication.py +0 -52
  240. prefect/_vendor/starlette/middleware/base.py +0 -220
  241. prefect/_vendor/starlette/middleware/cors.py +0 -176
  242. prefect/_vendor/starlette/middleware/errors.py +0 -265
  243. prefect/_vendor/starlette/middleware/exceptions.py +0 -74
  244. prefect/_vendor/starlette/middleware/gzip.py +0 -113
  245. prefect/_vendor/starlette/middleware/httpsredirect.py +0 -19
  246. prefect/_vendor/starlette/middleware/sessions.py +0 -82
  247. prefect/_vendor/starlette/middleware/trustedhost.py +0 -64
  248. prefect/_vendor/starlette/middleware/wsgi.py +0 -147
  249. prefect/_vendor/starlette/py.typed +0 -0
  250. prefect/_vendor/starlette/requests.py +0 -328
  251. prefect/_vendor/starlette/responses.py +0 -347
  252. prefect/_vendor/starlette/routing.py +0 -933
  253. prefect/_vendor/starlette/schemas.py +0 -154
  254. prefect/_vendor/starlette/staticfiles.py +0 -248
  255. prefect/_vendor/starlette/status.py +0 -199
  256. prefect/_vendor/starlette/templating.py +0 -231
  257. prefect/_vendor/starlette/testclient.py +0 -804
  258. prefect/_vendor/starlette/types.py +0 -30
  259. prefect/_vendor/starlette/websockets.py +0 -193
  260. prefect/blocks/kubernetes.py +0 -119
  261. prefect/deprecated/__init__.py +0 -0
  262. prefect/deprecated/data_documents.py +0 -350
  263. prefect/deprecated/packaging/__init__.py +0 -12
  264. prefect/deprecated/packaging/base.py +0 -96
  265. prefect/deprecated/packaging/docker.py +0 -146
  266. prefect/deprecated/packaging/file.py +0 -92
  267. prefect/deprecated/packaging/orion.py +0 -80
  268. prefect/deprecated/packaging/serializers.py +0 -171
  269. prefect/events/instrument.py +0 -135
  270. prefect/infrastructure/container.py +0 -824
  271. prefect/infrastructure/kubernetes.py +0 -920
  272. prefect/infrastructure/process.py +0 -289
  273. prefect/manifests.py +0 -20
  274. prefect/new_flow_engine.py +0 -449
  275. prefect/new_task_engine.py +0 -423
  276. prefect/pydantic/__init__.py +0 -76
  277. prefect/pydantic/main.py +0 -39
  278. prefect/software/__init__.py +0 -2
  279. prefect/software/base.py +0 -50
  280. prefect/software/conda.py +0 -199
  281. prefect/software/pip.py +0 -122
  282. prefect/software/python.py +0 -52
  283. prefect/task_server.py +0 -322
  284. prefect_client-2.20.4.dist-info/RECORD +0 -294
  285. /prefect/{_internal/pydantic/utilities → client/types}/__init__.py +0 -0
  286. /prefect/{_vendor → concurrency/v1}/__init__.py +0 -0
  287. {prefect_client-2.20.4.dist-info → prefect_client-3.0.0.dist-info}/LICENSE +0 -0
  288. {prefect_client-2.20.4.dist-info → prefect_client-3.0.0.dist-info}/top_level.txt +0 -0
@@ -1,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