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/__init__.py +5 -0
- wellapi/__main__.py +3 -0
- wellapi/applications.py +389 -0
- wellapi/awsmodel.py +17 -0
- wellapi/build/__init__.py +0 -0
- wellapi/build/cdk.py +141 -0
- wellapi/build/packager.py +82 -0
- wellapi/build/sam_openapi.py +10 -0
- wellapi/cli/__init__.py +0 -0
- wellapi/cli/main.py +67 -0
- wellapi/convertors.py +89 -0
- wellapi/datastructures.py +383 -0
- wellapi/dependencies/__init__.py +0 -0
- wellapi/dependencies/models.py +138 -0
- wellapi/dependencies/utils.py +923 -0
- wellapi/exceptions.py +53 -0
- wellapi/local/__init__.py +0 -0
- wellapi/local/reloader.py +94 -0
- wellapi/local/router.py +116 -0
- wellapi/local/server.py +154 -0
- wellapi/middleware/__init__.py +0 -0
- wellapi/middleware/base.py +18 -0
- wellapi/middleware/error.py +239 -0
- wellapi/middleware/exceptions.py +74 -0
- wellapi/middleware/main.py +26 -0
- wellapi/models.py +150 -0
- wellapi/openapi/__init__.py +0 -0
- wellapi/openapi/docs.py +344 -0
- wellapi/openapi/models.py +404 -0
- wellapi/openapi/utils.py +535 -0
- wellapi/params.py +481 -0
- wellapi/routing.py +248 -0
- wellapi/security.py +82 -0
- wellapi/utils.py +37 -0
- wellapi-0.2.1.dist-info/METADATA +32 -0
- wellapi-0.2.1.dist-info/RECORD +38 -0
- wellapi-0.2.1.dist-info/WHEEL +4 -0
- wellapi-0.2.1.dist-info/entry_points.txt +2 -0
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 []))))
|