wellapi 0.2.1__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.
wellapi/cli/main.py ADDED
@@ -0,0 +1,67 @@
1
+ import json
2
+
3
+ import click
4
+
5
+ from wellapi import WellApi
6
+ from wellapi.local.server import run_local_server
7
+ from wellapi.openapi.utils import get_openapi
8
+ from wellapi.utils import import_app, load_handlers
9
+
10
+ # ruff: noqa: W291
11
+ WELLAPI_ACII = """
12
+ _ _ _______ ___ ___ _______ _______ ___
13
+ | | _ | || || | | | | _ || || |
14
+ | || || || ___|| | | | | |_| || _ || |
15
+ | || |___ | | | | | || |_| || |
16
+ | || ___|| |___ | |___ | || ___|| |
17
+ | _ || |___ | || | | _ || | | |
18
+ |__| |__||_______||_______||_______| |__| |__||___| |___|
19
+ """
20
+
21
+
22
+ @click.group()
23
+ def cli():
24
+ click.echo(click.style(WELLAPI_ACII, fg="magenta"))
25
+
26
+
27
+ @cli.command()
28
+ @click.argument("app_srt", default="main:app")
29
+ @click.argument(
30
+ "handlers_dir", default="handlers", type=click.Path(exists=True, resolve_path=True)
31
+ )
32
+ def build(app_srt: str, handlers_dir: str):
33
+ app: WellApi = import_app(app_srt)
34
+ load_handlers(handlers_dir)
35
+
36
+ resp = get_openapi(
37
+ title=app.title,
38
+ version=app.version,
39
+ openapi_version="3.0.1",
40
+ description=app.description,
41
+ lambdas=app.lambdas,
42
+ tags=app.openapi_tags,
43
+ servers=app.servers,
44
+ )
45
+
46
+ with open("openapi.json", "w") as f:
47
+ json.dump(resp, f)
48
+
49
+
50
+ @cli.command()
51
+ @click.argument("app_srt", default="main:app")
52
+ @click.argument(
53
+ "handlers_dir", default="handlers", type=click.Path(exists=True, resolve_path=True)
54
+ )
55
+ @click.option("--host", default="127.0.0.1")
56
+ @click.option("--port", default=8000, type=click.INT)
57
+ @click.option(
58
+ "--autoreload/--no-autoreload",
59
+ default=True,
60
+ help="Automatically restart server when code changes.",
61
+ )
62
+ def run(app_srt: str, handlers_dir: str, host="127.0.0.1", port=8000, autoreload=True):
63
+ run_local_server(app_srt, handlers_dir, host, port, autoreload)
64
+
65
+
66
+ if __name__ == "__main__":
67
+ cli()
wellapi/convertors.py ADDED
@@ -0,0 +1,89 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ import typing
5
+ import uuid
6
+
7
+ T = typing.TypeVar("T")
8
+
9
+
10
+ class Convertor(typing.Generic[T]):
11
+ regex: typing.ClassVar[str] = ""
12
+
13
+ def convert(self, value: str) -> T:
14
+ raise NotImplementedError() # pragma: no cover
15
+
16
+ def to_string(self, value: T) -> str:
17
+ raise NotImplementedError() # pragma: no cover
18
+
19
+
20
+ class StringConvertor(Convertor[str]):
21
+ regex = "[^/]+"
22
+
23
+ def convert(self, value: str) -> str:
24
+ return value
25
+
26
+ def to_string(self, value: str) -> str:
27
+ value = str(value)
28
+ assert "/" not in value, "May not contain path separators"
29
+ assert value, "Must not be empty"
30
+ return value
31
+
32
+
33
+ class PathConvertor(Convertor[str]):
34
+ regex = ".*"
35
+
36
+ def convert(self, value: str) -> str:
37
+ return str(value)
38
+
39
+ def to_string(self, value: str) -> str:
40
+ return str(value)
41
+
42
+
43
+ class IntegerConvertor(Convertor[int]):
44
+ regex = "[0-9]+"
45
+
46
+ def convert(self, value: str) -> int:
47
+ return int(value)
48
+
49
+ def to_string(self, value: int) -> str:
50
+ value = int(value)
51
+ assert value >= 0, "Negative integers are not supported"
52
+ return str(value)
53
+
54
+
55
+ class FloatConvertor(Convertor[float]):
56
+ regex = r"[0-9]+(\.[0-9]+)?"
57
+
58
+ def convert(self, value: str) -> float:
59
+ return float(value)
60
+
61
+ def to_string(self, value: float) -> str:
62
+ value = float(value)
63
+ assert value >= 0.0, "Negative floats are not supported"
64
+ assert not math.isnan(value), "NaN values are not supported"
65
+ assert not math.isinf(value), "Infinite values are not supported"
66
+ return ("%0.20f" % value).rstrip("0").rstrip(".") # noqa: UP031
67
+
68
+
69
+ class UUIDConvertor(Convertor[uuid.UUID]):
70
+ regex = "[0-9a-fA-F]{8}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{12}"
71
+
72
+ def convert(self, value: str) -> uuid.UUID:
73
+ return uuid.UUID(value)
74
+
75
+ def to_string(self, value: uuid.UUID) -> str:
76
+ return str(value)
77
+
78
+
79
+ CONVERTOR_TYPES: dict[str, Convertor[typing.Any]] = {
80
+ "str": StringConvertor(),
81
+ "path": PathConvertor(),
82
+ "int": IntegerConvertor(),
83
+ "float": FloatConvertor(),
84
+ "uuid": UUIDConvertor(),
85
+ }
86
+
87
+
88
+ def register_url_convertor(key: str, convertor: Convertor[typing.Any]) -> None:
89
+ CONVERTOR_TYPES[key] = convertor
@@ -0,0 +1,383 @@
1
+ import typing
2
+ from urllib.parse import parse_qsl, urlencode
3
+
4
+ _KeyType = typing.TypeVar("_KeyType")
5
+ # Mapping keys are invariant but their values are covariant since
6
+ # you can only read them
7
+ # that is, you can't do `Mapping[str, Animal]()["fido"] = Dog()`
8
+ _CovariantValueType = typing.TypeVar("_CovariantValueType", covariant=True)
9
+
10
+
11
+ class ImmutableMultiDict(typing.Mapping[_KeyType, _CovariantValueType]):
12
+ _dict: dict[_KeyType, _CovariantValueType]
13
+
14
+ def __init__(
15
+ self,
16
+ *args: "ImmutableMultiDict[_KeyType, _CovariantValueType]"
17
+ | typing.Mapping[_KeyType, _CovariantValueType]
18
+ | typing.Iterable[tuple[_KeyType, _CovariantValueType]],
19
+ **kwargs: typing.Any,
20
+ ) -> None:
21
+ assert len(args) < 2, "Too many arguments."
22
+
23
+ value: typing.Any = args[0] if args else []
24
+ if kwargs:
25
+ value = (
26
+ ImmutableMultiDict(value).multi_items()
27
+ + ImmutableMultiDict(kwargs).multi_items()
28
+ )
29
+
30
+ if not value:
31
+ _items: list[tuple[typing.Any, typing.Any]] = []
32
+ elif hasattr(value, "multi_items"):
33
+ value = typing.cast(
34
+ ImmutableMultiDict[_KeyType, _CovariantValueType], value
35
+ )
36
+ _items = list(value.multi_items())
37
+ elif hasattr(value, "items"):
38
+ value = typing.cast(typing.Mapping[_KeyType, _CovariantValueType], value)
39
+ _items = list(value.items())
40
+ else:
41
+ value = typing.cast("list[tuple[typing.Any, typing.Any]]", value)
42
+ _items = list(value)
43
+
44
+ self._dict = dict(_items)
45
+ self._list = _items
46
+
47
+ def getlist(self, key: typing.Any) -> list[_CovariantValueType]:
48
+ return [item_value for item_key, item_value in self._list if item_key == key]
49
+
50
+ def keys(self) -> typing.KeysView[_KeyType]:
51
+ return self._dict.keys()
52
+
53
+ def values(self) -> typing.ValuesView[_CovariantValueType]:
54
+ return self._dict.values()
55
+
56
+ def items(self) -> typing.ItemsView[_KeyType, _CovariantValueType]:
57
+ return self._dict.items()
58
+
59
+ def multi_items(self) -> list[tuple[_KeyType, _CovariantValueType]]:
60
+ return list(self._list)
61
+
62
+ def __getitem__(self, key: _KeyType) -> _CovariantValueType:
63
+ return self._dict[key]
64
+
65
+ def __contains__(self, key: typing.Any) -> bool:
66
+ return key in self._dict
67
+
68
+ def __iter__(self) -> typing.Iterator[_KeyType]:
69
+ return iter(self.keys())
70
+
71
+ def __len__(self) -> int:
72
+ return len(self._dict)
73
+
74
+ def __eq__(self, other: typing.Any) -> bool:
75
+ if not isinstance(other, self.__class__):
76
+ return False
77
+ return sorted(self._list) == sorted(other._list)
78
+
79
+ def __repr__(self) -> str:
80
+ class_name = self.__class__.__name__
81
+ items = self.multi_items()
82
+ return f"{class_name}({items!r})"
83
+
84
+
85
+ class MultiDict(ImmutableMultiDict[typing.Any, typing.Any]):
86
+ def __setitem__(self, key: typing.Any, value: typing.Any) -> None:
87
+ self.setlist(key, [value])
88
+
89
+ def __delitem__(self, key: typing.Any) -> None:
90
+ self._list = [(k, v) for k, v in self._list if k != key]
91
+ del self._dict[key]
92
+
93
+ def pop(self, key: typing.Any, default: typing.Any = None) -> typing.Any:
94
+ self._list = [(k, v) for k, v in self._list if k != key]
95
+ return self._dict.pop(key, default)
96
+
97
+ def popitem(self) -> tuple[typing.Any, typing.Any]:
98
+ key, value = self._dict.popitem()
99
+ self._list = [(k, v) for k, v in self._list if k != key]
100
+ return key, value
101
+
102
+ def poplist(self, key: typing.Any) -> list[typing.Any]:
103
+ values = [v for k, v in self._list if k == key]
104
+ self.pop(key)
105
+ return values
106
+
107
+ def clear(self) -> None:
108
+ self._dict.clear()
109
+ self._list.clear()
110
+
111
+ def setdefault(self, key: typing.Any, default: typing.Any = None) -> typing.Any:
112
+ if key not in self:
113
+ self._dict[key] = default
114
+ self._list.append((key, default))
115
+
116
+ return self[key]
117
+
118
+ def setlist(self, key: typing.Any, values: list[typing.Any]) -> None:
119
+ if not values:
120
+ self.pop(key, None)
121
+ else:
122
+ existing_items = [(k, v) for (k, v) in self._list if k != key]
123
+ self._list = existing_items + [(key, value) for value in values]
124
+ self._dict[key] = values[-1]
125
+
126
+ def append(self, key: typing.Any, value: typing.Any) -> None:
127
+ self._list.append((key, value))
128
+ self._dict[key] = value
129
+
130
+ def update(
131
+ self,
132
+ *args: "MultiDict"
133
+ | typing.Mapping[typing.Any, typing.Any]
134
+ | list[tuple[typing.Any, typing.Any]],
135
+ **kwargs: typing.Any,
136
+ ) -> None:
137
+ value = MultiDict(*args, **kwargs)
138
+ existing_items = [(k, v) for (k, v) in self._list if k not in value.keys()]
139
+ self._list = existing_items + value.multi_items()
140
+ self._dict.update(value)
141
+
142
+
143
+ class QueryParams(ImmutableMultiDict[str, str]):
144
+ """
145
+ An immutable multidict.
146
+ """
147
+
148
+ def __init__(
149
+ self,
150
+ *args: ImmutableMultiDict[typing.Any, typing.Any]
151
+ | typing.Mapping[typing.Any, typing.Any]
152
+ | list[tuple[typing.Any, typing.Any]]
153
+ | str
154
+ | bytes,
155
+ **kwargs: typing.Any,
156
+ ) -> None:
157
+ assert len(args) < 2, "Too many arguments."
158
+
159
+ value = args[0] if args else []
160
+
161
+ if isinstance(value, str):
162
+ super().__init__(parse_qsl(value, keep_blank_values=True), **kwargs)
163
+ elif isinstance(value, bytes):
164
+ super().__init__(
165
+ parse_qsl(value.decode("latin-1"), keep_blank_values=True), **kwargs
166
+ )
167
+ else:
168
+ super().__init__(*args, **kwargs) # type: ignore[arg-type]
169
+ self._list = [(str(k), str(v)) for k, v in self._list]
170
+ self._dict = {str(k): str(v) for k, v in self._dict.items()}
171
+
172
+ def __str__(self) -> str:
173
+ return urlencode(self._list)
174
+
175
+ def __repr__(self) -> str:
176
+ class_name = self.__class__.__name__
177
+ query_string = str(self)
178
+ return f"{class_name}({query_string!r})"
179
+
180
+
181
+ class Headers(typing.Mapping[str, str]):
182
+ """
183
+ An immutable, case-insensitive multidict.
184
+ """
185
+
186
+ def __init__(
187
+ self,
188
+ headers: typing.Mapping[str, str] | None = None,
189
+ raw: list[tuple[bytes, bytes]] | None = None,
190
+ scope: typing.MutableMapping[str, typing.Any] | None = None,
191
+ ) -> None:
192
+ self._list: list[tuple[bytes, bytes]] = []
193
+ if headers is not None:
194
+ assert raw is None, 'Cannot set both "headers" and "raw".'
195
+ assert scope is None, 'Cannot set both "headers" and "scope".'
196
+ self._list = [
197
+ (key.lower().encode("latin-1"), value.encode("latin-1"))
198
+ for key, value in headers.items()
199
+ ]
200
+ elif raw is not None:
201
+ assert scope is None, 'Cannot set both "raw" and "scope".'
202
+ self._list = raw
203
+ elif scope is not None:
204
+ # scope["headers"] isn't necessarily a list
205
+ # it might be a tuple or other iterable
206
+ self._list = scope["headers"] = list(scope["headers"])
207
+
208
+ @property
209
+ def raw(self) -> list[tuple[bytes, bytes]]:
210
+ return list(self._list)
211
+
212
+ def keys(self) -> list[str]: # type: ignore[override]
213
+ return [key.decode("latin-1") for key, value in self._list]
214
+
215
+ def values(self) -> list[str]: # type: ignore[override]
216
+ return [value.decode("latin-1") for key, value in self._list]
217
+
218
+ def items(self) -> list[tuple[str, str]]: # type: ignore[override]
219
+ return [
220
+ (key.decode("latin-1"), value.decode("latin-1"))
221
+ for key, value in self._list
222
+ ]
223
+
224
+ def getlist(self, key: str) -> list[str]:
225
+ get_header_key = key.lower().encode("latin-1")
226
+ return [
227
+ item_value.decode("latin-1")
228
+ for item_key, item_value in self._list
229
+ if item_key == get_header_key
230
+ ]
231
+
232
+ def mutablecopy(self) -> "MutableHeaders":
233
+ return MutableHeaders(raw=self._list[:])
234
+
235
+ def __getitem__(self, key: str) -> str:
236
+ get_header_key = key.lower().encode("latin-1")
237
+ for header_key, header_value in self._list:
238
+ if header_key == get_header_key:
239
+ return header_value.decode("latin-1")
240
+ raise KeyError(key)
241
+
242
+ def __contains__(self, key: typing.Any) -> bool:
243
+ get_header_key = key.lower().encode("latin-1")
244
+ for header_key, _header_value in self._list:
245
+ if header_key == get_header_key:
246
+ return True
247
+ return False
248
+
249
+ def __iter__(self) -> typing.Iterator[typing.Any]:
250
+ return iter(self.keys())
251
+
252
+ def __len__(self) -> int:
253
+ return len(self._list)
254
+
255
+ def __eq__(self, other: typing.Any) -> bool:
256
+ if not isinstance(other, Headers):
257
+ return False
258
+ return sorted(self._list) == sorted(other._list)
259
+
260
+ def __repr__(self) -> str:
261
+ class_name = self.__class__.__name__
262
+ as_dict = dict(self.items())
263
+ if len(as_dict) == len(self):
264
+ return f"{class_name}({as_dict!r})"
265
+ return f"{class_name}(raw={self.raw!r})"
266
+
267
+
268
+ class MutableHeaders(Headers):
269
+ def __setitem__(self, key: str, value: str) -> None:
270
+ """
271
+ Set the header `key` to `value`, removing any duplicate entries.
272
+ Retains insertion order.
273
+ """
274
+ set_key = key.lower().encode("latin-1")
275
+ set_value = value.encode("latin-1")
276
+
277
+ found_indexes: list[int] = []
278
+ for idx, (item_key, _item_value) in enumerate(self._list):
279
+ if item_key == set_key:
280
+ found_indexes.append(idx)
281
+
282
+ for idx in reversed(found_indexes[1:]):
283
+ del self._list[idx]
284
+
285
+ if found_indexes:
286
+ idx = found_indexes[0]
287
+ self._list[idx] = (set_key, set_value)
288
+ else:
289
+ self._list.append((set_key, set_value))
290
+
291
+ def __delitem__(self, key: str) -> None:
292
+ """
293
+ Remove the header `key`.
294
+ """
295
+ del_key = key.lower().encode("latin-1")
296
+
297
+ pop_indexes: list[int] = []
298
+ for idx, (item_key, _item_value) in enumerate(self._list):
299
+ if item_key == del_key:
300
+ pop_indexes.append(idx)
301
+
302
+ for idx in reversed(pop_indexes):
303
+ del self._list[idx]
304
+
305
+ def __ior__(self, other: typing.Mapping[str, str]) -> "MutableHeaders":
306
+ if not isinstance(other, typing.Mapping):
307
+ raise TypeError(f"Expected a mapping but got {other.__class__.__name__}")
308
+ self.update(other)
309
+ return self
310
+
311
+ def __or__(self, other: typing.Mapping[str, str]) -> "MutableHeaders":
312
+ if not isinstance(other, typing.Mapping):
313
+ raise TypeError(f"Expected a mapping but got {other.__class__.__name__}")
314
+ new = self.mutablecopy()
315
+ new.update(other)
316
+ return new
317
+
318
+ @property
319
+ def raw(self) -> list[tuple[bytes, bytes]]:
320
+ return self._list
321
+
322
+ def setdefault(self, key: str, value: str) -> str:
323
+ """
324
+ If the header `key` does not exist, then set it to `value`.
325
+ Returns the header value.
326
+ """
327
+ set_key = key.lower().encode("latin-1")
328
+ set_value = value.encode("latin-1")
329
+
330
+ for _idx, (item_key, item_value) in enumerate(self._list):
331
+ if item_key == set_key:
332
+ return item_value.decode("latin-1")
333
+ self._list.append((set_key, set_value))
334
+ return value
335
+
336
+ def update(self, other: typing.Mapping[str, str]) -> None:
337
+ for key, val in other.items():
338
+ self[key] = val
339
+
340
+ def append(self, key: str, value: str) -> None:
341
+ """
342
+ Append a header, preserving any duplicate entries.
343
+ """
344
+ append_key = key.lower().encode("latin-1")
345
+ append_value = value.encode("latin-1")
346
+ self._list.append((append_key, append_value))
347
+
348
+ def add_vary_header(self, vary: str) -> None:
349
+ existing = self.get("vary")
350
+ if existing is not None:
351
+ vary = ", ".join([existing, vary])
352
+ self["vary"] = vary
353
+
354
+
355
+ class DefaultPlaceholder:
356
+ """
357
+ You shouldn't use this class directly.
358
+
359
+ It's used internally to recognize when a default value has been overwritten, even
360
+ if the overridden default value was truthy.
361
+ """
362
+
363
+ def __init__(self, value: typing.Any):
364
+ self.value = value
365
+
366
+ def __bool__(self) -> bool:
367
+ return bool(self.value)
368
+
369
+ def __eq__(self, o: object) -> bool:
370
+ return isinstance(o, DefaultPlaceholder) and o.value == self.value
371
+
372
+
373
+ DefaultType = typing.TypeVar("DefaultType")
374
+
375
+
376
+ def Default(value: DefaultType) -> DefaultType:
377
+ """
378
+ You shouldn't use this function directly.
379
+
380
+ It's used internally to recognize when a default value has been overwritten, even
381
+ if the overridden default value was truthy.
382
+ """
383
+ return DefaultPlaceholder(value) # type: ignore
File without changes
@@ -0,0 +1,138 @@
1
+ from collections.abc import Callable, Sequence
2
+ from dataclasses import dataclass, field
3
+ from typing import (
4
+ Annotated,
5
+ Any,
6
+ Literal,
7
+ )
8
+
9
+ from pydantic import TypeAdapter, ValidationError
10
+ from pydantic.fields import FieldInfo
11
+ from pydantic.main import IncEx
12
+ from pydantic_core import PydanticUndefined
13
+
14
+ from wellapi.security import SecurityBase
15
+
16
+
17
+ def _regenerate_error_with_loc(
18
+ *, errors: Sequence[Any], loc_prefix: tuple[str | int, ...]
19
+ ) -> list[dict[str, Any]]:
20
+ updated_loc_errors: list[Any] = [
21
+ {**err, "loc": loc_prefix + err.get("loc", ())} for err in errors
22
+ ]
23
+
24
+ return updated_loc_errors
25
+
26
+
27
+ @dataclass
28
+ class ModelField:
29
+ field_info: FieldInfo
30
+ name: str
31
+ mode: Literal["validation", "serialization"] = "validation"
32
+
33
+ @property
34
+ def alias(self) -> str:
35
+ a = self.field_info.alias
36
+ return a if a is not None else self.name
37
+
38
+ @property
39
+ def required(self) -> bool:
40
+ return self.field_info.is_required()
41
+
42
+ @property
43
+ def default(self) -> Any:
44
+ return self.get_default()
45
+
46
+ @property
47
+ def type_(self) -> Any:
48
+ return self.field_info.annotation
49
+
50
+ def __post_init__(self) -> None:
51
+ self._type_adapter: TypeAdapter[Any] = TypeAdapter(
52
+ Annotated[self.field_info.annotation, self.field_info]
53
+ )
54
+
55
+ def get_default(self) -> Any:
56
+ if self.field_info.is_required():
57
+ return PydanticUndefined
58
+ return self.field_info.get_default(call_default_factory=True)
59
+
60
+ def validate(
61
+ self,
62
+ value: Any,
63
+ values: dict[str, Any] = {}, # noqa: B006
64
+ *,
65
+ loc: tuple[int | str, ...] = (),
66
+ ) -> tuple[Any, list[dict[str, Any]] | None]:
67
+ try:
68
+ return (
69
+ self._type_adapter.validate_python(value, from_attributes=True),
70
+ None,
71
+ )
72
+ except ValidationError as exc:
73
+ return None, _regenerate_error_with_loc(
74
+ errors=exc.errors(include_url=False), loc_prefix=loc
75
+ )
76
+
77
+ def serialize(
78
+ self,
79
+ value: Any,
80
+ *,
81
+ mode: Literal["json", "python"] = "json",
82
+ include: IncEx | None = None,
83
+ exclude: IncEx | None = None,
84
+ by_alias: bool = True,
85
+ exclude_unset: bool = False,
86
+ exclude_defaults: bool = False,
87
+ exclude_none: bool = False,
88
+ ) -> Any:
89
+ # What calls this code passes a value that already called
90
+ # self._type_adapter.validate_python(value)
91
+ return self._type_adapter.dump_python(
92
+ value,
93
+ mode=mode,
94
+ include=include,
95
+ exclude=exclude,
96
+ by_alias=by_alias,
97
+ exclude_unset=exclude_unset,
98
+ exclude_defaults=exclude_defaults,
99
+ exclude_none=exclude_none,
100
+ )
101
+
102
+ def __hash__(self) -> int:
103
+ # Each ModelField is unique for our purposes, to allow making a dict from
104
+ # ModelField to its JSON Schema.
105
+ return id(self)
106
+
107
+
108
+ @dataclass
109
+ class SecurityRequirement:
110
+ security_scheme: SecurityBase
111
+ scopes: Sequence[str] | None = None
112
+
113
+
114
+ @dataclass
115
+ class Dependant:
116
+ path_params: list[ModelField] = field(default_factory=list)
117
+ query_params: list[ModelField] = field(default_factory=list)
118
+ header_params: list[ModelField] = field(default_factory=list)
119
+ cookie_params: list[ModelField] = field(default_factory=list)
120
+ body_params: list[ModelField] = field(default_factory=list)
121
+ dependencies: list["Dependant"] = field(default_factory=list)
122
+ security_requirements: list[SecurityRequirement] = field(default_factory=list)
123
+ name: str | None = None
124
+ call: Callable[..., Any] | None = None
125
+ request_param_name: str | None = None
126
+ request_sqs_param_name: str | None = None
127
+ websocket_param_name: str | None = None
128
+ http_connection_param_name: str | None = None
129
+ response_param_name: str | None = None
130
+ background_tasks_param_name: str | None = None
131
+ security_scopes_param_name: str | None = None
132
+ security_scopes: list[str] | None = None
133
+ use_cache: bool = True
134
+ path: str | None = None
135
+ cache_key: tuple[Callable[..., Any] | None, tuple[str, ...]] = field(init=False)
136
+
137
+ def __post_init__(self) -> None:
138
+ self.cache_key = (self.call, tuple(sorted(set(self.security_scopes or []))))