-
Notifications
You must be signed in to change notification settings - Fork 29
Expand file tree
/
Copy pathtraits.py
More file actions
352 lines (249 loc) · 11.2 KB
/
traits.py
File metadata and controls
352 lines (249 loc) · 11.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
# This ruff check warns against using the assert statement, which can be stripped out
# when running Python with certain (common) optimization settings. Assert is used here
# for trait values. Since these are always generated, we can be fairly confident that
# they're correct regardless, so it's okay if the checks are stripped out.
# ruff: noqa: S101
from collections.abc import Mapping
from dataclasses import dataclass, field
from enum import Enum
from typing import TYPE_CHECKING, ClassVar
from .shapes import ShapeID
from .types import PathPattern, TimestampFormat
if TYPE_CHECKING:
from .documents import DocumentValue
@dataclass(kw_only=True, frozen=True, slots=True)
class DynamicTrait:
"""A component that can be attached to a schema to describe additional information
about it.
Typed traits can be used by creating a :py:class:`Trait` subclass.
"""
id: ShapeID
"""The ID of the trait."""
document_value: "DocumentValue" = None
"""The value of the trait."""
@dataclass(init=False, frozen=True)
class Trait:
"""A component that can be attached to a schema to describe additional information
about it.
This is a base class that registers subclasses. Any known subclasses will
automatically be used when constructing schemas. Any unknown traits may instead be
created as a :py:class:`DynamicTrait`.
The `id` property of subclasses is set during subclass creation by
`__init_subclass__`, so it is not necessary for subclasses to set it manually.
"""
_REGISTRY: ClassVar[dict[ShapeID, type["Trait"]]] = {}
id: ClassVar[ShapeID]
"""The ID of the trait."""
document_value: "DocumentValue" = None
"""The value of the trait as a DocumentValue."""
def __init_subclass__(cls, id: ShapeID) -> None:
cls.id = id
Trait._REGISTRY[id] = cls
def __init__(self, value: "DocumentValue | DynamicTrait" = None):
if type(self) is Trait:
raise TypeError(
"Only subclasses of Trait may be directly instantiated. "
"Use DynamicTrait for traits without a concrete class."
)
if isinstance(value, DynamicTrait):
if value.id != self.id:
raise ValueError(
f"Attempted to instantiate an instance of {type(self)} from an "
f"invalid ID. Expected {self.id} but found {value.id}."
)
# Note that setattr is needed because it's a frozen (read-only) dataclass
object.__setattr__(self, "document_value", value.document_value)
else:
object.__setattr__(self, "document_value", value)
# Dynamically creates a subclass instance based on the trait id
@staticmethod
def new(id: ShapeID, value: "DocumentValue" = None) -> "Trait | DynamicTrait":
"""Dynamically create a new trait of the given ID.
If the ID corresponds to a known Trait class, that class will be instantiated
and returned. Otherwise, a :py:class:`DynamicTrait` will be returned.
:returns: A trait of the given ID with the given value.
"""
if (cls := Trait._REGISTRY.get(id, None)) is not None:
return cls(value)
return DynamicTrait(id=id, document_value=value)
@dataclass(init=False, frozen=True)
class DefaultTrait(Trait, id=ShapeID("smithy.appi#default")):
@property
def value(self) -> "DocumentValue":
return self.document_value
@dataclass(init=False, frozen=True)
class SparseTrait(Trait, id=ShapeID("smithy.api#sparse")):
def __post_init__(self):
assert self.document_value is None
@dataclass(init=False, frozen=True)
class TimestampFormatTrait(Trait, id=ShapeID("smithy.api#timestampFormat")):
format: TimestampFormat
def __init__(self, value: "DocumentValue | DynamicTrait" = None):
super().__init__(value)
assert isinstance(self.document_value, str)
object.__setattr__(self, "format", TimestampFormat(self.document_value))
class ErrorFault(Enum):
CLIENT = "client"
SERVER = "server"
@dataclass(init=False, frozen=True)
class ErrorTrait(Trait, id=ShapeID("smithy.api#error")):
fault: ErrorFault
def __init__(self, value: "DocumentValue | DynamicTrait" = None):
super().__init__(value)
assert isinstance(self.document_value, str)
object.__setattr__(self, "fault", ErrorFault(self.document_value))
@dataclass(init=False, frozen=True)
class RequiredTrait(Trait, id=ShapeID("smithy.api#required")):
def __post_init__(self):
assert self.document_value is None
@dataclass(init=False, frozen=True)
class InternalTrait(Trait, id=ShapeID("smithy.api#internal")):
def __post_init__(self):
assert self.document_value is None
@dataclass(init=False, frozen=True)
class SensitiveTrait(Trait, id=ShapeID("smithy.api#sensitive")):
def __post_init__(self):
assert self.document_value is None
@dataclass(init=False, frozen=True)
class StreamingTrait(Trait, id=ShapeID("smithy.api#streaming")):
def __post_init__(self):
assert self.document_value is None
@dataclass(init=False, frozen=True)
class RequiresLengthTrait(Trait, id=ShapeID("smithy.api#requiresLength")):
def __post_init__(self):
assert self.document_value is None
@dataclass(init=False, frozen=True)
class UnitTypeTrait(Trait, id=ShapeID("smithy.api#UnitTypeTrait")):
def __post_init__(self):
assert self.document_value is None
@dataclass(init=False, frozen=True)
class MediaTypeTrait(Trait, id=ShapeID("smithy.api#mediaType")):
document_value: str | None = None
def __post_init__(self):
assert isinstance(self.document_value, str)
@property
def value(self) -> str:
return self.document_value # type: ignore
@dataclass(init=False, frozen=True)
class EventHeaderTrait(Trait, id=ShapeID("smithy.api#eventHeader")):
def __post_init__(self):
assert self.document_value is None
@dataclass(init=False, frozen=True)
class EventPayloadTrait(Trait, id=ShapeID("smithy.api#eventPayload")):
def __post_init__(self):
assert self.document_value is None
@dataclass(init=False, frozen=True)
class JSONNameTrait(Trait, id=ShapeID("smithy.api#jsonName")):
document_value: str | None = None
def __post_init__(self):
assert isinstance(self.document_value, str)
@property
def value(self) -> str:
return self.document_value # type: ignore
@dataclass(init=False, frozen=True)
class IdempotencyTokenTrait(Trait, id=ShapeID("smithy.api#IdempotencyToken")):
def __post_init__(self):
assert self.document_value is None
# TODO: Get all this moved over to the http package
@dataclass(init=False, frozen=True)
class HTTPTrait(Trait, id=ShapeID("smithy.api#http")):
path: PathPattern = field(repr=False, hash=False, compare=False)
code: int = field(repr=False, hash=False, compare=False)
query: str | None = field(default=None, repr=False, hash=False, compare=False)
def __init__(self, value: "DocumentValue | DynamicTrait" = None):
super().__init__(value)
assert isinstance(self.document_value, Mapping)
assert isinstance(self.document_value["method"], str)
code = self.document_value.get("code", 200)
assert isinstance(code, int)
object.__setattr__(self, "code", code)
uri = self.document_value["uri"]
assert isinstance(uri, str)
parts = uri.split("?", 1)
object.__setattr__(self, "path", PathPattern(parts[0]))
object.__setattr__(self, "query", parts[1] if len(parts) == 2 else None)
@property
def method(self) -> str:
return self.document_value["method"] # type: ignore
@dataclass(init=False, frozen=True)
class HTTPErrorTrait(Trait, id=ShapeID("smithy.api#httpError")):
def __post_init__(self):
assert isinstance(self.document_value, int)
@property
def code(self) -> int:
return self.document_value # type: ignore
@dataclass(init=False, frozen=True)
class HTTPHeaderTrait(Trait, id=ShapeID("smithy.api#httpHeader")):
def __post_init__(self):
assert isinstance(self.document_value, str)
@property
def key(self) -> str:
return self.document_value # type: ignore
@dataclass(init=False, frozen=True)
class HTTPLabelTrait(Trait, id=ShapeID("smithy.api#httpLabel")):
def __post_init__(self):
assert self.document_value is None
@dataclass(init=False, frozen=True)
class HTTPPayloadTrait(Trait, id=ShapeID("smithy.api#httpPayload")):
def __post_init__(self):
assert self.document_value is None
@dataclass(init=False, frozen=True)
class HTTPPrefixHeadersTrait(Trait, id=ShapeID("smithy.api#httpPrefixHeaders")):
def __post_init__(self):
assert isinstance(self.document_value, str)
@property
def prefix(self) -> str:
return self.document_value # type: ignore
@dataclass(init=False, frozen=True)
class HTTPQueryTrait(Trait, id=ShapeID("smithy.api#httpQuery")):
def __post_init__(self):
assert isinstance(self.document_value, str)
@property
def key(self) -> str:
return self.document_value # type: ignore
@dataclass(init=False, frozen=True)
class HTTPQueryParamsTrait(Trait, id=ShapeID("smithy.api#httpQueryParams")):
def __post_init__(self):
assert self.document_value is None
@dataclass(init=False, frozen=True)
class HTTPResponseCodeTrait(Trait, id=ShapeID("smithy.api#httpResponseCode")):
def __post_init__(self):
assert self.document_value is None
@dataclass(init=False, frozen=True)
class HTTPChecksumRequiredTrait(Trait, id=ShapeID("smithy.api#httpChecksumRequired")):
def __post_init__(self):
assert self.document_value is None
@dataclass(init=False, frozen=True)
class EndpointTrait(Trait, id=ShapeID("smithy.api#endpoint")):
def __post_init__(self):
assert isinstance(self.document_value, str)
@property
def host_prefix(self) -> str:
return self.document_value["hostPrefix"] # type: ignore
@dataclass(init=False, frozen=True)
class HostLabelTrait(Trait, id=ShapeID("smithy.api#hostLabel")):
def __post_init__(self):
assert self.document_value is None
class APIKeyLocation(Enum):
"""The locations that the api key could be placed in the signed request."""
HEADER = "header"
QUERY = "query"
@dataclass(init=False, frozen=True)
class HTTPAPIKeyAuthTrait(Trait, id=ShapeID("smithy.api#httpApiKeyAuth")):
location: APIKeyLocation = field(repr=False, hash=False, compare=False)
def __init__(self, value: "DocumentValue | DynamicTrait" = None):
super().__init__(value)
assert isinstance(self.document_value, Mapping)
assert isinstance(self.document_value["in"], str)
assert isinstance(self.document_value["name"], str)
assert isinstance(self.document_value.get("scheme"), str | None)
location = self.document_value["in"]
object.__setattr__(self, "location", APIKeyLocation(location))
@property
def name(self) -> str:
return self.document_value["name"] # type: ignore
@property
def scheme(self) -> str | None:
return self.document_value.get("scheme") # type: ignore