smithy-python 0.1.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.
- smithy_python/__init__.py +5 -0
- smithy_python/__main__.py +7 -0
- smithy_python/cli.py +180 -0
- smithy_python/environment.py +40 -0
- smithy_python/exceptions.py +27 -0
- smithy_python/model/__init__.py +56 -0
- smithy_python/model/_loader.py +573 -0
- smithy_python/model/_model.py +134 -0
- smithy_python/model/_prelude.py +60 -0
- smithy_python/model/_shape_id.py +115 -0
- smithy_python/model/_shapes.py +251 -0
- smithy_python/py.typed +0 -0
- smithy_python/selection.py +157 -0
- smithy_python-0.1.0.dist-info/METADATA +47 -0
- smithy_python-0.1.0.dist-info/RECORD +18 -0
- smithy_python-0.1.0.dist-info/WHEEL +4 -0
- smithy_python-0.1.0.dist-info/entry_points.txt +2 -0
- smithy_python-0.1.0.dist-info/licenses/NOTICE +1 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""The embedded subset of the Smithy prelude.
|
|
4
|
+
|
|
5
|
+
Only the shapes that model members and relationships can target are embedded.
|
|
6
|
+
Trait-definition shapes are intentionally omitted: trait keys are not resolved
|
|
7
|
+
as references, and later stages never generate or inspect trait definitions.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections.abc import Mapping
|
|
13
|
+
from types import MappingProxyType
|
|
14
|
+
from typing import Any, Final
|
|
15
|
+
|
|
16
|
+
_P: Final = "smithy.api#"
|
|
17
|
+
|
|
18
|
+
_SIMPLE: Final = (
|
|
19
|
+
("String", "string"),
|
|
20
|
+
("Blob", "blob"),
|
|
21
|
+
("BigInteger", "bigInteger"),
|
|
22
|
+
("BigDecimal", "bigDecimal"),
|
|
23
|
+
("Timestamp", "timestamp"),
|
|
24
|
+
("Document", "document"),
|
|
25
|
+
("Boolean", "boolean"),
|
|
26
|
+
("Byte", "byte"),
|
|
27
|
+
("Short", "short"),
|
|
28
|
+
("Integer", "integer"),
|
|
29
|
+
("Long", "long"),
|
|
30
|
+
("Float", "float"),
|
|
31
|
+
("Double", "double"),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
_PRIMITIVE: Final = (
|
|
35
|
+
("PrimitiveBoolean", "boolean", False),
|
|
36
|
+
("PrimitiveByte", "byte", 0),
|
|
37
|
+
("PrimitiveShort", "short", 0),
|
|
38
|
+
("PrimitiveInteger", "integer", 0),
|
|
39
|
+
("PrimitiveLong", "long", 0),
|
|
40
|
+
("PrimitiveFloat", "float", 0),
|
|
41
|
+
("PrimitiveDouble", "double", 0),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def prelude_ast() -> Mapping[str, Mapping[str, Any]]:
|
|
46
|
+
"""Return a fresh JSON AST ``shapes`` mapping for the embedded prelude."""
|
|
47
|
+
shapes: dict[str, Mapping[str, Any]] = {}
|
|
48
|
+
for name, type_name in _SIMPLE:
|
|
49
|
+
shapes[_P + name] = {"type": type_name}
|
|
50
|
+
for name, type_name, default in _PRIMITIVE:
|
|
51
|
+
shapes[_P + name] = {
|
|
52
|
+
"type": type_name,
|
|
53
|
+
"traits": {_P + "default": default},
|
|
54
|
+
}
|
|
55
|
+
shapes[_P + "Unit"] = {
|
|
56
|
+
"type": "structure",
|
|
57
|
+
"members": {},
|
|
58
|
+
"traits": {_P + "unitType": {}},
|
|
59
|
+
}
|
|
60
|
+
return MappingProxyType(shapes)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Shape identifiers."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Final, Self
|
|
10
|
+
|
|
11
|
+
from ..exceptions import InvalidShapeIdError
|
|
12
|
+
|
|
13
|
+
_IDENTIFIER: Final = r"_*[A-Za-z][A-Za-z0-9_]*"
|
|
14
|
+
_IDENTIFIER_RE: Final = re.compile(_IDENTIFIER)
|
|
15
|
+
_NAMESPACE_RE: Final = re.compile(rf"{_IDENTIFIER}(?:\.{_IDENTIFIER})*")
|
|
16
|
+
|
|
17
|
+
PRELUDE_NAMESPACE: Final = "smithy.api"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class ShapeId:
|
|
22
|
+
"""A Smithy shape ID: ``namespace#Name`` or ``namespace#Name$member``.
|
|
23
|
+
|
|
24
|
+
Shape IDs are hashable and totally ordered by (namespace, name, member),
|
|
25
|
+
with the root shape sorting before its members.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
namespace: str
|
|
29
|
+
name: str
|
|
30
|
+
member: str | None = None
|
|
31
|
+
|
|
32
|
+
def __post_init__(self) -> None:
|
|
33
|
+
if not _NAMESPACE_RE.fullmatch(self.namespace):
|
|
34
|
+
raise InvalidShapeIdError(
|
|
35
|
+
f"Invalid shape ID namespace {self.namespace!r} in {self._raw()!r}"
|
|
36
|
+
)
|
|
37
|
+
if not _IDENTIFIER_RE.fullmatch(self.name):
|
|
38
|
+
raise InvalidShapeIdError(
|
|
39
|
+
f"Invalid shape ID name {self.name!r} in {self._raw()!r}"
|
|
40
|
+
)
|
|
41
|
+
if self.member is not None and not _IDENTIFIER_RE.fullmatch(self.member):
|
|
42
|
+
raise InvalidShapeIdError(
|
|
43
|
+
f"Invalid shape ID member {self.member!r} in {self._raw()!r}"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def from_string(cls, value: str, *, default_namespace: str | None = None) -> Self:
|
|
48
|
+
"""Parse an absolute or relative shape ID.
|
|
49
|
+
|
|
50
|
+
Relative IDs (no ``#``) are resolved against ``default_namespace``;
|
|
51
|
+
without one they are rejected.
|
|
52
|
+
"""
|
|
53
|
+
if "#" in value:
|
|
54
|
+
namespace, _, rest = value.partition("#")
|
|
55
|
+
if "#" in rest:
|
|
56
|
+
raise InvalidShapeIdError(f"Invalid shape ID {value!r}")
|
|
57
|
+
elif default_namespace is not None:
|
|
58
|
+
namespace, rest = default_namespace, value
|
|
59
|
+
else:
|
|
60
|
+
raise InvalidShapeIdError(
|
|
61
|
+
f"Invalid shape ID {value!r}: expected an absolute shape ID "
|
|
62
|
+
"of the form namespace#Name"
|
|
63
|
+
)
|
|
64
|
+
name, sep, member = rest.partition("$")
|
|
65
|
+
try:
|
|
66
|
+
return cls(namespace, name, member if sep else None)
|
|
67
|
+
except InvalidShapeIdError:
|
|
68
|
+
raise InvalidShapeIdError(f"Invalid shape ID {value!r}") from None
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def is_member(self) -> bool:
|
|
72
|
+
"""Whether this ID refers to a member."""
|
|
73
|
+
return self.member is not None
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def root(self) -> ShapeId:
|
|
77
|
+
"""This ID without its member component."""
|
|
78
|
+
if self.member is None:
|
|
79
|
+
return self
|
|
80
|
+
return ShapeId(self.namespace, self.name)
|
|
81
|
+
|
|
82
|
+
def with_member(self, member: str) -> ShapeId:
|
|
83
|
+
"""Return the ID of ``member`` within this shape."""
|
|
84
|
+
return ShapeId(self.namespace, self.name, member)
|
|
85
|
+
|
|
86
|
+
def __str__(self) -> str:
|
|
87
|
+
return self._raw()
|
|
88
|
+
|
|
89
|
+
def _raw(self) -> str:
|
|
90
|
+
base = f"{self.namespace}#{self.name}"
|
|
91
|
+
return base if self.member is None else f"{base}${self.member}"
|
|
92
|
+
|
|
93
|
+
def _key(self) -> tuple[str, str, bool, str]:
|
|
94
|
+
return (self.namespace, self.name, self.member is not None, self.member or "")
|
|
95
|
+
|
|
96
|
+
def __lt__(self, other: ShapeId) -> bool:
|
|
97
|
+
return self._key() < other._key()
|
|
98
|
+
|
|
99
|
+
def __le__(self, other: ShapeId) -> bool:
|
|
100
|
+
return self._key() <= other._key()
|
|
101
|
+
|
|
102
|
+
def __gt__(self, other: ShapeId) -> bool:
|
|
103
|
+
return self._key() > other._key()
|
|
104
|
+
|
|
105
|
+
def __ge__(self, other: ShapeId) -> bool:
|
|
106
|
+
return self._key() >= other._key()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def as_shape_id(
|
|
110
|
+
value: ShapeId | str, *, default_namespace: str | None = None
|
|
111
|
+
) -> ShapeId:
|
|
112
|
+
"""Coerce a string or ShapeId to a ShapeId."""
|
|
113
|
+
if isinstance(value, ShapeId):
|
|
114
|
+
return value
|
|
115
|
+
return ShapeId.from_string(value, default_namespace=default_namespace)
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Shape types and the flattened, immutable shape classes."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from enum import StrEnum
|
|
10
|
+
from types import MappingProxyType
|
|
11
|
+
from typing import Final
|
|
12
|
+
|
|
13
|
+
from ._shape_id import PRELUDE_NAMESPACE, ShapeId, as_shape_id
|
|
14
|
+
|
|
15
|
+
type Node = None | bool | int | float | str | tuple[Node, ...] | Mapping[str, Node]
|
|
16
|
+
"""A deeply immutable JSON value: arrays are tuples, objects read-only mappings."""
|
|
17
|
+
|
|
18
|
+
type JsonValue = (
|
|
19
|
+
None | bool | int | float | str | list[JsonValue] | dict[str, JsonValue]
|
|
20
|
+
)
|
|
21
|
+
"""A mutable JSON value, as produced by :func:`to_json`."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def to_json(node: Node) -> JsonValue:
|
|
25
|
+
"""Return a mutable copy of a node, e.g. for ``json.dumps`` or editing."""
|
|
26
|
+
if isinstance(node, Mapping):
|
|
27
|
+
return {k: to_json(v) for k, v in node.items()}
|
|
28
|
+
if isinstance(node, tuple):
|
|
29
|
+
return [to_json(v) for v in node]
|
|
30
|
+
return node
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ShapeType(StrEnum):
|
|
34
|
+
"""Every shape type in the Smithy 2.0 semantic model, including members."""
|
|
35
|
+
|
|
36
|
+
BLOB = "blob"
|
|
37
|
+
BOOLEAN = "boolean"
|
|
38
|
+
STRING = "string"
|
|
39
|
+
BYTE = "byte"
|
|
40
|
+
SHORT = "short"
|
|
41
|
+
INTEGER = "integer"
|
|
42
|
+
LONG = "long"
|
|
43
|
+
FLOAT = "float"
|
|
44
|
+
DOUBLE = "double"
|
|
45
|
+
BIG_INTEGER = "bigInteger"
|
|
46
|
+
BIG_DECIMAL = "bigDecimal"
|
|
47
|
+
TIMESTAMP = "timestamp"
|
|
48
|
+
DOCUMENT = "document"
|
|
49
|
+
ENUM = "enum"
|
|
50
|
+
INT_ENUM = "intEnum"
|
|
51
|
+
LIST = "list"
|
|
52
|
+
MAP = "map"
|
|
53
|
+
STRUCTURE = "structure"
|
|
54
|
+
UNION = "union"
|
|
55
|
+
MEMBER = "member"
|
|
56
|
+
SERVICE = "service"
|
|
57
|
+
RESOURCE = "resource"
|
|
58
|
+
OPERATION = "operation"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
SIMPLE_TYPES: Final = frozenset(
|
|
62
|
+
{
|
|
63
|
+
ShapeType.BLOB,
|
|
64
|
+
ShapeType.BOOLEAN,
|
|
65
|
+
ShapeType.STRING,
|
|
66
|
+
ShapeType.BYTE,
|
|
67
|
+
ShapeType.SHORT,
|
|
68
|
+
ShapeType.INTEGER,
|
|
69
|
+
ShapeType.LONG,
|
|
70
|
+
ShapeType.FLOAT,
|
|
71
|
+
ShapeType.DOUBLE,
|
|
72
|
+
ShapeType.BIG_INTEGER,
|
|
73
|
+
ShapeType.BIG_DECIMAL,
|
|
74
|
+
ShapeType.TIMESTAMP,
|
|
75
|
+
ShapeType.DOCUMENT,
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
MIXIN_TRAIT: Final = ShapeId(PRELUDE_NAMESPACE, "mixin")
|
|
80
|
+
UNIT: Final = ShapeId(PRELUDE_NAMESPACE, "Unit")
|
|
81
|
+
|
|
82
|
+
_EMPTY_TRAITS: Final[Mapping[ShapeId, Node]] = MappingProxyType({})
|
|
83
|
+
_EMPTY_MEMBERS: Final[Mapping[str, MemberShape]] = MappingProxyType({})
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(frozen=True, slots=True, kw_only=True, eq=False)
|
|
87
|
+
class Shape:
|
|
88
|
+
"""Base class of every shape.
|
|
89
|
+
|
|
90
|
+
Shapes are already mixin-flattened: ``members`` and ``traits`` include
|
|
91
|
+
everything inherited from mixins, in a deterministic order. Instances are
|
|
92
|
+
immutable; trait values are deeply frozen :data:`Node` values (use
|
|
93
|
+
:func:`to_json` for a mutable copy). Equality is identity.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
id: ShapeId
|
|
97
|
+
type: ShapeType
|
|
98
|
+
traits: Mapping[ShapeId, Node] = _EMPTY_TRAITS
|
|
99
|
+
"""Trait ID to JSON node value, in merge order (see the loader docs)."""
|
|
100
|
+
mixins: tuple[ShapeId, ...] = ()
|
|
101
|
+
"""The mixins this shape declared, in declaration order.
|
|
102
|
+
|
|
103
|
+
Informational only: members, traits, and properties already include
|
|
104
|
+
everything inherited from these mixins.
|
|
105
|
+
"""
|
|
106
|
+
members: Mapping[str, MemberShape] = _EMPTY_MEMBERS
|
|
107
|
+
"""Members by name, in order: inherited mixin members, then local ones."""
|
|
108
|
+
|
|
109
|
+
def has_trait(self, trait: ShapeId | str) -> bool:
|
|
110
|
+
"""Whether the shape has a trait; relative names resolve to smithy.api."""
|
|
111
|
+
return _trait_id(trait) in self.traits
|
|
112
|
+
|
|
113
|
+
def get_trait(self, trait: ShapeId | str) -> Node:
|
|
114
|
+
"""Return a trait value or None if absent (use has_trait for presence)."""
|
|
115
|
+
return self.traits.get(_trait_id(trait))
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def is_mixin(self) -> bool:
|
|
119
|
+
"""Whether the shape is marked with ``@mixin``."""
|
|
120
|
+
return MIXIN_TRAIT in self.traits
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _trait_id(trait: ShapeId | str) -> ShapeId:
|
|
124
|
+
return as_shape_id(trait, default_namespace=PRELUDE_NAMESPACE)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass(frozen=True, slots=True, kw_only=True, eq=False)
|
|
128
|
+
class SimpleShape(Shape):
|
|
129
|
+
"""A blob, boolean, string, numeric, timestamp, or document shape."""
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@dataclass(frozen=True, slots=True, kw_only=True, eq=False)
|
|
133
|
+
class MemberShape(Shape):
|
|
134
|
+
"""A member of an aggregate shape; its ID is rooted at its container."""
|
|
135
|
+
|
|
136
|
+
type: ShapeType = ShapeType.MEMBER
|
|
137
|
+
target: ShapeId
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def container(self) -> ShapeId:
|
|
141
|
+
"""ID of the shape that contains this member."""
|
|
142
|
+
return self.id.root
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def name(self) -> str:
|
|
146
|
+
"""The member name."""
|
|
147
|
+
assert self.id.member is not None # noqa: S101 - invariant
|
|
148
|
+
return self.id.member
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass(frozen=True, slots=True, kw_only=True, eq=False)
|
|
152
|
+
class ListShape(Shape):
|
|
153
|
+
"""A list shape; ``members`` contains exactly ``member``."""
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def member(self) -> MemberShape:
|
|
157
|
+
return self.members["member"]
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@dataclass(frozen=True, slots=True, kw_only=True, eq=False)
|
|
161
|
+
class MapShape(Shape):
|
|
162
|
+
"""A map shape; ``members`` contains ``key`` then ``value``."""
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def key(self) -> MemberShape:
|
|
166
|
+
return self.members["key"]
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def value(self) -> MemberShape:
|
|
170
|
+
return self.members["value"]
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@dataclass(frozen=True, slots=True, kw_only=True, eq=False)
|
|
174
|
+
class StructureShape(Shape):
|
|
175
|
+
"""A structure shape."""
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@dataclass(frozen=True, slots=True, kw_only=True, eq=False)
|
|
179
|
+
class UnionShape(Shape):
|
|
180
|
+
"""A union shape."""
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@dataclass(frozen=True, slots=True, kw_only=True, eq=False)
|
|
184
|
+
class EnumShape(Shape):
|
|
185
|
+
"""A string enum; member values live in ``smithy.api#enumValue``."""
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@dataclass(frozen=True, slots=True, kw_only=True, eq=False)
|
|
189
|
+
class IntEnumShape(Shape):
|
|
190
|
+
"""An integer enum; member values live in ``smithy.api#enumValue``."""
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@dataclass(frozen=True, slots=True, kw_only=True, eq=False)
|
|
194
|
+
class ServiceShape(Shape):
|
|
195
|
+
"""A service shape."""
|
|
196
|
+
|
|
197
|
+
version: str | None = None
|
|
198
|
+
operations: tuple[ShapeId, ...] = ()
|
|
199
|
+
resources: tuple[ShapeId, ...] = ()
|
|
200
|
+
errors: tuple[ShapeId, ...] = ()
|
|
201
|
+
rename: Mapping[ShapeId, str] = field(default_factory=lambda: MappingProxyType({}))
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@dataclass(frozen=True, slots=True, kw_only=True, eq=False)
|
|
205
|
+
class ResourceShape(Shape):
|
|
206
|
+
"""A resource shape."""
|
|
207
|
+
|
|
208
|
+
identifiers: Mapping[str, ShapeId] = field(
|
|
209
|
+
default_factory=lambda: MappingProxyType({})
|
|
210
|
+
)
|
|
211
|
+
properties: Mapping[str, ShapeId] = field(
|
|
212
|
+
default_factory=lambda: MappingProxyType({})
|
|
213
|
+
)
|
|
214
|
+
create: ShapeId | None = None
|
|
215
|
+
put: ShapeId | None = None
|
|
216
|
+
read: ShapeId | None = None
|
|
217
|
+
update: ShapeId | None = None
|
|
218
|
+
delete: ShapeId | None = None
|
|
219
|
+
list: ShapeId | None = None
|
|
220
|
+
operations: tuple[ShapeId, ...] = ()
|
|
221
|
+
collection_operations: tuple[ShapeId, ...] = ()
|
|
222
|
+
resources: tuple[ShapeId, ...] = ()
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@dataclass(frozen=True, slots=True, kw_only=True, eq=False)
|
|
226
|
+
class OperationShape(Shape):
|
|
227
|
+
"""An operation shape.
|
|
228
|
+
|
|
229
|
+
An absent ``input`` or ``output`` is ``smithy.api#Unit``, matching Smithy's
|
|
230
|
+
semantic model; explicit and implicit Unit are indistinguishable.
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
input: ShapeId = UNIT
|
|
234
|
+
output: ShapeId = UNIT
|
|
235
|
+
errors: tuple[ShapeId, ...] = ()
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
SHAPE_CLASSES: Final[Mapping[ShapeType, type[Shape]]] = MappingProxyType(
|
|
239
|
+
{
|
|
240
|
+
**dict.fromkeys(SIMPLE_TYPES, SimpleShape),
|
|
241
|
+
ShapeType.LIST: ListShape,
|
|
242
|
+
ShapeType.MAP: MapShape,
|
|
243
|
+
ShapeType.STRUCTURE: StructureShape,
|
|
244
|
+
ShapeType.UNION: UnionShape,
|
|
245
|
+
ShapeType.ENUM: EnumShape,
|
|
246
|
+
ShapeType.INT_ENUM: IntEnumShape,
|
|
247
|
+
ShapeType.SERVICE: ServiceShape,
|
|
248
|
+
ShapeType.RESOURCE: ResourceShape,
|
|
249
|
+
ShapeType.OPERATION: OperationShape,
|
|
250
|
+
}
|
|
251
|
+
)
|
smithy_python/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Service and data-shape selection on a loaded semantic model."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from collections.abc import Iterator
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
from .exceptions import InvalidInvocationError, InvalidShapeIdError, ModelError
|
|
11
|
+
from .model import (
|
|
12
|
+
MemberShape,
|
|
13
|
+
Model,
|
|
14
|
+
OperationShape,
|
|
15
|
+
ResourceShape,
|
|
16
|
+
ServiceShape,
|
|
17
|
+
Shape,
|
|
18
|
+
ShapeId,
|
|
19
|
+
ShapeType,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True, slots=True)
|
|
24
|
+
class Selection:
|
|
25
|
+
"""Selected service and eligible data shapes in original model order."""
|
|
26
|
+
|
|
27
|
+
service: ServiceShape | None
|
|
28
|
+
shapes: tuple[Shape, ...]
|
|
29
|
+
excluded_count: int
|
|
30
|
+
"""Eligible data shapes omitted by the service closure (zero without a service)."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def select_shapes(
|
|
34
|
+
model: Model, *, service_id: str | None = None, require_service: bool = False
|
|
35
|
+
) -> Selection:
|
|
36
|
+
"""Select data for generation from an already loaded, flattened model.
|
|
37
|
+
|
|
38
|
+
Service selection failures raise ``InvalidInvocationError``. Without a
|
|
39
|
+
service, case-insensitive data-shape name conflicts raise ``ModelError``.
|
|
40
|
+
This stage neither validates Smithy semantics nor writes output.
|
|
41
|
+
"""
|
|
42
|
+
service = resolve_service(
|
|
43
|
+
model, service_id=service_id, require_service=require_service
|
|
44
|
+
)
|
|
45
|
+
eligible = tuple(
|
|
46
|
+
shape
|
|
47
|
+
for shape in model.iter_shapes()
|
|
48
|
+
if shape.type
|
|
49
|
+
not in (
|
|
50
|
+
ShapeType.SERVICE,
|
|
51
|
+
ShapeType.OPERATION,
|
|
52
|
+
ShapeType.RESOURCE,
|
|
53
|
+
ShapeType.MEMBER,
|
|
54
|
+
)
|
|
55
|
+
and not shape.is_mixin
|
|
56
|
+
and not shape.has_trait("trait")
|
|
57
|
+
)
|
|
58
|
+
if service is None:
|
|
59
|
+
_check_names(eligible)
|
|
60
|
+
return Selection(service=None, shapes=eligible, excluded_count=0)
|
|
61
|
+
|
|
62
|
+
visited: set[ShapeId] = set()
|
|
63
|
+
pending = [service.id]
|
|
64
|
+
while pending:
|
|
65
|
+
shape_id = pending.pop()
|
|
66
|
+
if shape_id in visited:
|
|
67
|
+
continue
|
|
68
|
+
visited.add(shape_id)
|
|
69
|
+
pending.extend(_references(model.get_shape(shape_id)))
|
|
70
|
+
|
|
71
|
+
selected = tuple(shape for shape in eligible if shape.id in visited)
|
|
72
|
+
return Selection(
|
|
73
|
+
service=service, shapes=selected, excluded_count=len(eligible) - len(selected)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _references(shape: Shape) -> Iterator[ShapeId]:
|
|
78
|
+
# Only semantic relationships are edges, never trait nodes, rename entries,
|
|
79
|
+
# or mixin declarations (the loader has already flattened their contents).
|
|
80
|
+
for member in shape.members.values():
|
|
81
|
+
yield member.target
|
|
82
|
+
if isinstance(shape, MemberShape):
|
|
83
|
+
yield shape.target
|
|
84
|
+
elif isinstance(shape, ServiceShape):
|
|
85
|
+
yield from shape.operations
|
|
86
|
+
yield from shape.resources
|
|
87
|
+
yield from shape.errors
|
|
88
|
+
elif isinstance(shape, OperationShape):
|
|
89
|
+
yield shape.input
|
|
90
|
+
yield shape.output
|
|
91
|
+
yield from shape.errors
|
|
92
|
+
elif isinstance(shape, ResourceShape):
|
|
93
|
+
yield from shape.identifiers.values()
|
|
94
|
+
yield from shape.properties.values()
|
|
95
|
+
for operation in (
|
|
96
|
+
shape.create,
|
|
97
|
+
shape.put,
|
|
98
|
+
shape.read,
|
|
99
|
+
shape.update,
|
|
100
|
+
shape.delete,
|
|
101
|
+
shape.list,
|
|
102
|
+
):
|
|
103
|
+
if operation is not None:
|
|
104
|
+
yield operation
|
|
105
|
+
yield from shape.operations
|
|
106
|
+
yield from shape.collection_operations
|
|
107
|
+
yield from shape.resources
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _check_names(shapes: tuple[Shape, ...]) -> None:
|
|
111
|
+
by_name: dict[str, list[ShapeId]] = {}
|
|
112
|
+
for shape in shapes:
|
|
113
|
+
by_name.setdefault(shape.id.name.casefold(), []).append(shape.id)
|
|
114
|
+
conflicts = [ids for ids in by_name.values() if len(ids) > 1]
|
|
115
|
+
if conflicts:
|
|
116
|
+
details = "; ".join(
|
|
117
|
+
", ".join(str(shape_id) for shape_id in ids) for ids in conflicts
|
|
118
|
+
)
|
|
119
|
+
raise ModelError(f"Case-insensitive data shape name conflicts: {details}")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def resolve_service(
|
|
123
|
+
model: Model, *, service_id: str | None = None, require_service: bool = False
|
|
124
|
+
) -> ServiceShape | None:
|
|
125
|
+
"""Resolve an absolute service ID or the sole non-mixin service.
|
|
126
|
+
|
|
127
|
+
Invalid or ambiguous selections raise :class:`InvalidInvocationError`.
|
|
128
|
+
"""
|
|
129
|
+
if service_id is not None:
|
|
130
|
+
try:
|
|
131
|
+
shape_id = ShapeId.from_string(service_id)
|
|
132
|
+
except InvalidShapeIdError as error:
|
|
133
|
+
raise InvalidInvocationError(f"--service: {error}") from error
|
|
134
|
+
if shape_id.is_member:
|
|
135
|
+
raise InvalidInvocationError(
|
|
136
|
+
f"--service {service_id!r} is a member ID, not a service"
|
|
137
|
+
)
|
|
138
|
+
shape = model.find_shape(shape_id)
|
|
139
|
+
if shape is None:
|
|
140
|
+
raise InvalidInvocationError(f"--service {service_id!r} is not defined")
|
|
141
|
+
if not isinstance(shape, ServiceShape):
|
|
142
|
+
raise InvalidInvocationError(f"--service {service_id!r} is not a service")
|
|
143
|
+
if shape.is_mixin:
|
|
144
|
+
raise InvalidInvocationError(f"--service {service_id!r} is a mixin")
|
|
145
|
+
return shape
|
|
146
|
+
|
|
147
|
+
services = model.services()
|
|
148
|
+
if len(services) > 1:
|
|
149
|
+
candidates = ", ".join(str(service.id) for service in services)
|
|
150
|
+
raise InvalidInvocationError(
|
|
151
|
+
f"Multiple services found; select one with --service: {candidates}"
|
|
152
|
+
)
|
|
153
|
+
if services:
|
|
154
|
+
return services[0]
|
|
155
|
+
if require_service:
|
|
156
|
+
raise InvalidInvocationError("Client generation requires a service")
|
|
157
|
+
return None
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: smithy-python
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Smithy code generator for Python clients and types.
|
|
5
|
+
Project-URL: Changelog, https://github.com/smithy-lang/smithy-python/blob/develop/packages/smithy-python/CHANGELOG.md
|
|
6
|
+
Project-URL: Code, https://github.com/smithy-lang/smithy-python/tree/develop/packages/smithy-python/
|
|
7
|
+
Project-URL: Issue tracker, https://github.com/smithy-lang/smithy-python/issues
|
|
8
|
+
Author: Amazon Web Services
|
|
9
|
+
License: Apache License 2.0
|
|
10
|
+
License-File: NOTICE
|
|
11
|
+
Keywords: codegen,sdk,smithy
|
|
12
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
+
Classifier: Natural Language :: English
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Programming Language :: Python :: Free Threading :: 2 - Beta
|
|
24
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
25
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
26
|
+
Requires-Python: >=3.12
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# smithy-python
|
|
30
|
+
|
|
31
|
+
> [!WARNING]
|
|
32
|
+
> This package is an experimental scaffold. It does not generate code yet. The
|
|
33
|
+
> Java generator in the repository's `codegen` directory remains authoritative.
|
|
34
|
+
|
|
35
|
+
`smithy-python` will provide Python-native code generation for Smithy models.
|
|
36
|
+
The initial command-line interface exposes the planned client and types generation
|
|
37
|
+
commands so that their top-level shape can be developed independently from the
|
|
38
|
+
generator implementation.
|
|
39
|
+
|
|
40
|
+
```console
|
|
41
|
+
smithy-python generate client [OPTIONS]
|
|
42
|
+
smithy-python generate types [OPTIONS]
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
After validating their invocation options, both generation commands currently exit
|
|
46
|
+
with an error explaining that generation has not been implemented. The package is
|
|
47
|
+
included in workspace builds to validate its packaging and entry points.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
smithy_python/__init__.py,sha256=TYc2ZwzNfYTsMhGUavAo7icqsC0mLg-n72wdROvgAsM,190
|
|
2
|
+
smithy_python/__main__.py,sha256=k8QDgjDBeX6t-Kk934IctxdYkfSUyVpVDah4yKBaQcw,187
|
|
3
|
+
smithy_python/cli.py,sha256=lcHI12FVGAN0OX5ZWzOnwLOvZAexTHV5m8pzSb1g3ds,5982
|
|
4
|
+
smithy_python/environment.py,sha256=4N-E_8P_a-YNGoa0XtzQifeoSDdWdVCh-LgWn6wdkas,1359
|
|
5
|
+
smithy_python/exceptions.py,sha256=2HEVrOGGymv1OyC0izM8YJq1sCKKWIovbk_UHAu2Pj8,835
|
|
6
|
+
smithy_python/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
smithy_python/selection.py,sha256=oIAn4gRN9eVN3VDjuSMXNrzUhzdb-TgSdIsrD8vOmqw,5373
|
|
8
|
+
smithy_python/model/__init__.py,sha256=kq_nBh2wldOrOqsxlQQjPtqEuiJCIoodBh9laIFIs_M,1268
|
|
9
|
+
smithy_python/model/_loader.py,sha256=pRUkx9mJzeHp2HecoC_aliMy3XU6AYG6c3aPCrkMV-I,21226
|
|
10
|
+
smithy_python/model/_model.py,sha256=dbPvZ6ZEp8B2N6vuHifX2qLuSbhWJHIgY8avx8GQz1M,4872
|
|
11
|
+
smithy_python/model/_prelude.py,sha256=UUjJ29cL5WciRFqqNh2Zj9I52TUVCyzYIuN2RIKbOok,1814
|
|
12
|
+
smithy_python/model/_shape_id.py,sha256=Jky_-TwiZd1k-SKRqqqmydGfhrar8ITQxir22YoBQgw,3942
|
|
13
|
+
smithy_python/model/_shapes.py,sha256=hzL0654EMxKkfOkOvd7OA6-d4JHhXz3r92XqRfoY1Mo,7603
|
|
14
|
+
smithy_python-0.1.0.dist-info/METADATA,sha256=t6rpLxKqChGqOajmHP_Uz8FuysD-zDxNFSl3tJG3spE,2128
|
|
15
|
+
smithy_python-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
16
|
+
smithy_python-0.1.0.dist-info/entry_points.txt,sha256=MbnKGy68WZXMhKsryaaGzekHxS8racdfA6mF5VS4QxI,57
|
|
17
|
+
smithy_python-0.1.0.dist-info/licenses/NOTICE,sha256=1CkO1kwu3Q_OHYTj-d-yiBJA_lNN73a4zSntavaD4oc,67
|
|
18
|
+
smithy_python-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|