smithy-aws-core 0.0.1__tar.gz

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.
@@ -0,0 +1,36 @@
1
+ # Eclipse
2
+ .classpath
3
+ .project
4
+ .settings/
5
+
6
+ # Intellij
7
+ .idea/
8
+ *.iml
9
+ *.iws
10
+
11
+ # VS Code
12
+ .vscode/
13
+
14
+ # Mac
15
+ .DS_Store
16
+
17
+ # Maven
18
+ target/
19
+ **/dependency-reduced-pom.xml
20
+
21
+ # Gradle
22
+ **/.gradle
23
+ build/
24
+ */out/
25
+ **/*/out/
26
+
27
+ # Python
28
+ __pycache__
29
+ *.swp
30
+ .mypy_cache
31
+ .coverage
32
+ coverage.xml
33
+ htmlcov
34
+ *.egg-info
35
+ dist
36
+ .venv
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ * <Add new items here>
6
+
7
+ ## v0.0.1
8
+
9
+ ### Feature
10
+ * Added support for Instance Metadata Service (IMDS) credential resolution.
11
+ * Added basic endpoint support.
12
+ * Added basic User Agent support.
13
+ * Added basic AWS specific protocol support for RestJson1 and HTTP bindings.
@@ -0,0 +1 @@
1
+ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: smithy-aws-core
3
+ Version: 0.0.1
4
+ Summary: Core Smithy components for AWS services and protocols.
5
+ License-File: NOTICE
6
+ Requires-Python: >=3.12
7
+ Requires-Dist: aws-sdk-signers
8
+ Requires-Dist: smithy-core
9
+ Requires-Dist: smithy-http
10
+ Description-Content-Type: text/markdown
11
+
12
+ # smithy-aws-core
13
+
14
+ This is the core package that provides AWS specific interfaces for both client and server
15
+ tooling generated by Smithy.
@@ -0,0 +1,4 @@
1
+ # smithy-aws-core
2
+
3
+ This is the core package that provides AWS specific interfaces for both client and server
4
+ tooling generated by Smithy.
@@ -0,0 +1,23 @@
1
+ [project]
2
+ name = "smithy-aws-core"
3
+ version = "0.0.1"
4
+ description = "Core Smithy components for AWS services and protocols."
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = [
8
+ "smithy-core",
9
+ "smithy-http",
10
+ "aws-sdk-signers"
11
+ ]
12
+
13
+ [build-system]
14
+ requires = ["hatchling"]
15
+ build-backend = "hatchling.build"
16
+
17
+ [tool.hatch.build]
18
+ exclude = [
19
+ "tests",
20
+ ]
21
+
22
+ [tool.ruff]
23
+ src = ["src"]
@@ -0,0 +1,6 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import importlib.metadata
5
+
6
+ __version__: str = importlib.metadata.version("smithy-aws-core")
@@ -0,0 +1,2 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,28 @@
1
+ from typing import Final
2
+
3
+ from smithy_core.codecs import Codec
4
+ from smithy_core.shapes import ShapeID
5
+ from smithy_http.aio.protocols import HttpBindingClientProtocol
6
+ from smithy_json import JSONCodec
7
+
8
+ from ..traits import RestJson1Trait
9
+
10
+
11
+ class RestJsonClientProtocol(HttpBindingClientProtocol):
12
+ """An implementation of the aws.protocols#restJson1 protocol."""
13
+
14
+ _id: ShapeID = RestJson1Trait.id
15
+ _codec: JSONCodec = JSONCodec()
16
+ _contentType: Final = "application/json"
17
+
18
+ @property
19
+ def id(self) -> ShapeID:
20
+ return self._id
21
+
22
+ @property
23
+ def payload_codec(self) -> Codec:
24
+ return self._codec
25
+
26
+ @property
27
+ def content_type(self) -> str:
28
+ return self._contentType
@@ -0,0 +1,6 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from .sigv4 import SigV4AuthScheme
5
+
6
+ __all__ = ("SigV4AuthScheme",)
@@ -0,0 +1,54 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ from dataclasses import dataclass
4
+ from typing import Protocol
5
+
6
+ from aws_sdk_signers import AsyncSigV4Signer, SigV4SigningProperties
7
+ from smithy_core.aio.interfaces.identity import IdentityResolver
8
+ from smithy_core.exceptions import SmithyIdentityException
9
+ from smithy_core.interfaces.identity import IdentityProperties
10
+ from smithy_http.aio.interfaces.auth import HTTPAuthScheme, HTTPSigner
11
+
12
+ from ..identity import AWSCredentialsIdentity
13
+
14
+
15
+ class SigV4Config(Protocol):
16
+ aws_credentials_identity_resolver: (
17
+ IdentityResolver[AWSCredentialsIdentity, IdentityProperties] | None
18
+ )
19
+
20
+
21
+ @dataclass(init=False)
22
+ class SigV4AuthScheme(
23
+ HTTPAuthScheme[
24
+ AWSCredentialsIdentity, SigV4Config, IdentityProperties, SigV4SigningProperties
25
+ ]
26
+ ):
27
+ """SigV4 AuthScheme."""
28
+
29
+ scheme_id: str = "aws.auth#sigv4"
30
+ signer: HTTPSigner[AWSCredentialsIdentity, SigV4SigningProperties]
31
+
32
+ def __init__(
33
+ self,
34
+ *,
35
+ signer: HTTPSigner[AWSCredentialsIdentity, SigV4SigningProperties]
36
+ | None = None,
37
+ ) -> None:
38
+ """Constructor.
39
+
40
+ :param identity_resolver: The identity resolver to extract the api key identity.
41
+ :param signer: The signer used to sign the request.
42
+ """
43
+ # TODO: There are type mismatches in the signature of the "sign" method.
44
+ self.signer = signer or AsyncSigV4Signer() # type: ignore
45
+
46
+ def identity_resolver(
47
+ self, *, config: SigV4Config
48
+ ) -> IdentityResolver[AWSCredentialsIdentity, IdentityProperties]:
49
+ if not config.aws_credentials_identity_resolver:
50
+ raise SmithyIdentityException(
51
+ "Attempted to use SigV4 auth, but aws_credentials_identity_resolver was not "
52
+ "set on the config."
53
+ )
54
+ return config.aws_credentials_identity_resolver
@@ -0,0 +1,11 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ from .environment import EnvironmentCredentialsResolver
4
+ from .imds import IMDSCredentialsResolver
5
+ from .static import StaticCredentialsResolver
6
+
7
+ __all__ = (
8
+ "EnvironmentCredentialsResolver",
9
+ "IMDSCredentialsResolver",
10
+ "StaticCredentialsResolver",
11
+ )
@@ -0,0 +1,43 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ import os
4
+
5
+ from smithy_core.aio.interfaces.identity import IdentityResolver
6
+ from smithy_core.exceptions import SmithyIdentityException
7
+ from smithy_core.interfaces.identity import IdentityProperties
8
+
9
+ from ..identity import AWSCredentialsIdentity
10
+
11
+
12
+ class EnvironmentCredentialsResolver(
13
+ IdentityResolver[AWSCredentialsIdentity, IdentityProperties]
14
+ ):
15
+ """Resolves AWS Credentials from system environment variables."""
16
+
17
+ def __init__(self):
18
+ self._credentials = None
19
+
20
+ async def get_identity(
21
+ self, *, identity_properties: IdentityProperties
22
+ ) -> AWSCredentialsIdentity:
23
+ if self._credentials is not None:
24
+ return self._credentials
25
+
26
+ access_key_id = os.getenv("AWS_ACCESS_KEY_ID")
27
+ secret_access_key = os.getenv("AWS_SECRET_ACCESS_KEY")
28
+ session_token = os.getenv("AWS_SESSION_TOKEN")
29
+ account_id = os.getenv("AWS_ACCOUNT_ID")
30
+
31
+ if access_key_id is None or secret_access_key is None:
32
+ raise SmithyIdentityException(
33
+ "AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are required"
34
+ )
35
+
36
+ self._credentials = AWSCredentialsIdentity(
37
+ access_key_id=access_key_id,
38
+ secret_access_key=secret_access_key,
39
+ session_token=session_token,
40
+ account_id=account_id,
41
+ )
42
+
43
+ return self._credentials
@@ -0,0 +1,237 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ import asyncio
4
+ import json
5
+ from dataclasses import dataclass
6
+ from datetime import UTC, datetime, timedelta
7
+ from types import MappingProxyType
8
+ from typing import Literal
9
+
10
+ from smithy_core import URI
11
+ from smithy_core.aio.interfaces.identity import IdentityResolver
12
+ from smithy_core.exceptions import SmithyIdentityException
13
+ from smithy_core.interfaces.identity import IdentityProperties
14
+ from smithy_core.interfaces.retries import RetryStrategy
15
+ from smithy_core.retries import SimpleRetryStrategy
16
+ from smithy_http import Field, Fields
17
+ from smithy_http.aio import HTTPRequest
18
+ from smithy_http.aio.interfaces import HTTPClient
19
+
20
+ from .. import __version__
21
+ from ..identity import AWSCredentialsIdentity
22
+
23
+ _USER_AGENT_FIELD = Field(
24
+ name="User-Agent",
25
+ values=[f"aws-sdk-python-imds-client/{__version__}"],
26
+ )
27
+
28
+
29
+ @dataclass(init=False)
30
+ class Config:
31
+ """Configuration for EC2Metadata."""
32
+
33
+ _HOST_MAPPING = MappingProxyType(
34
+ {"IPv4": "169.254.169.254", "IPv6": "[fd00:ec2::254]"}
35
+ )
36
+ _MIN_TTL = 5
37
+ _MAX_TTL = 21600
38
+
39
+ retry_strategy: RetryStrategy
40
+ endpoint_uri: URI
41
+ endpoint_mode: Literal["IPv4", "IPv6"]
42
+ token_ttl: int
43
+
44
+ def __init__(
45
+ self,
46
+ *,
47
+ retry_strategy: RetryStrategy | None = None,
48
+ endpoint_uri: URI | None = None,
49
+ endpoint_mode: Literal["IPv4", "IPv6"] = "IPv4",
50
+ token_ttl: int = _MAX_TTL,
51
+ ec2_instance_profile_name: str | None = None,
52
+ ):
53
+ # TODO: Implement retries.
54
+ self.retry_strategy = retry_strategy or SimpleRetryStrategy(max_attempts=3)
55
+ self.endpoint_mode = endpoint_mode
56
+ self.endpoint_uri = self._resolve_endpoint(endpoint_uri, endpoint_mode)
57
+ self.token_ttl = self._validate_token_ttl(token_ttl)
58
+ self.ec2_instance_profile_name = ec2_instance_profile_name
59
+
60
+ def _validate_token_ttl(self, ttl: int) -> int:
61
+ if not self._MIN_TTL <= ttl <= self._MAX_TTL:
62
+ raise ValueError(
63
+ f"Token TTL must be between {self._MIN_TTL} and {self._MAX_TTL} seconds."
64
+ )
65
+ return ttl
66
+
67
+ def _resolve_endpoint(
68
+ self, endpoint_uri: URI | None, endpoint_mode: Literal["IPv4", "IPv6"]
69
+ ) -> URI:
70
+ if endpoint_uri is not None:
71
+ return endpoint_uri
72
+
73
+ return URI(
74
+ scheme="http",
75
+ host=self._HOST_MAPPING.get(endpoint_mode, self._HOST_MAPPING["IPv4"]),
76
+ port=80,
77
+ )
78
+
79
+
80
+ class Token:
81
+ """Represents an IMDSv2 session token with a value and method for checking
82
+ expiration."""
83
+
84
+ def __init__(self, value: str, ttl: int):
85
+ self._value = value
86
+ self._ttl = ttl
87
+ self._created_time = datetime.now()
88
+
89
+ def is_expired(self) -> bool:
90
+ return datetime.now() - self._created_time >= timedelta(seconds=self._ttl)
91
+
92
+ @property
93
+ def value(self) -> str:
94
+ return self._value
95
+
96
+
97
+ class TokenCache:
98
+ """Holds the token needed to fetch instance metadata.
99
+
100
+ In addition, it knows how to refresh itself.
101
+ """
102
+
103
+ _TOKEN_PATH = "/latest/api/token" # noqa: S105
104
+
105
+ def __init__(self, http_client: HTTPClient, config: Config):
106
+ self._http_client = http_client
107
+ self._config = config
108
+ self._base_uri = config.endpoint_uri
109
+ self._refresh_lock = asyncio.Lock()
110
+ self._token = None
111
+
112
+ def _should_refresh(self) -> bool:
113
+ return self._token is None or self._token.is_expired()
114
+
115
+ async def _refresh(self) -> None:
116
+ async with self._refresh_lock:
117
+ if not self._should_refresh():
118
+ return
119
+ headers = Fields(
120
+ [
121
+ _USER_AGENT_FIELD,
122
+ Field(
123
+ name="x-aws-ec2-metadata-token-ttl-seconds",
124
+ values=[str(self._config.token_ttl)],
125
+ ),
126
+ ]
127
+ )
128
+ request = HTTPRequest(
129
+ method="PUT",
130
+ destination=URI(
131
+ scheme=self._base_uri.scheme,
132
+ host=self._base_uri.host,
133
+ port=self._base_uri.port,
134
+ path=self._TOKEN_PATH,
135
+ ),
136
+ fields=headers,
137
+ )
138
+ response = await self._http_client.send(request)
139
+ token_value = await response.consume_body_async()
140
+ self._token = Token(token_value.decode("utf-8"), self._config.token_ttl)
141
+
142
+ async def get_token(self) -> Token:
143
+ if self._should_refresh():
144
+ await self._refresh()
145
+ assert self._token is not None # noqa: S101
146
+ return self._token
147
+
148
+
149
+ class EC2Metadata:
150
+ def __init__(self, http_client: HTTPClient, config: Config | None = None):
151
+ self._http_client = http_client
152
+ self._config = config or Config()
153
+ self._token_cache = TokenCache(
154
+ http_client=self._http_client, config=self._config
155
+ )
156
+
157
+ async def get(self, *, path: str) -> str:
158
+ token = await self._token_cache.get_token()
159
+ headers = Fields(
160
+ [
161
+ _USER_AGENT_FIELD,
162
+ Field(
163
+ name="x-aws-ec2-metadata-token",
164
+ values=[token.value],
165
+ ),
166
+ ]
167
+ )
168
+ request = HTTPRequest(
169
+ method="GET",
170
+ destination=URI(
171
+ scheme=self._config.endpoint_uri.scheme,
172
+ host=self._config.endpoint_uri.host,
173
+ port=self._config.endpoint_uri.port,
174
+ path=path,
175
+ ),
176
+ fields=headers,
177
+ )
178
+ response = await self._http_client.send(request=request)
179
+ body = await response.consume_body_async()
180
+ return body.decode("utf-8")
181
+
182
+
183
+ class IMDSCredentialsResolver(
184
+ IdentityResolver[AWSCredentialsIdentity, IdentityProperties]
185
+ ):
186
+ """Resolves AWS Credentials from an EC2 Instance Metadata Service (IMDS) client."""
187
+
188
+ _METADATA_PATH_BASE = "/latest/meta-data/iam/security-credentials"
189
+
190
+ def __init__(self, http_client: HTTPClient, config: Config | None = None):
191
+ # TODO: Respect IMDS specific config values from aws shared config file and environment.
192
+ self._http_client = http_client
193
+ self._ec2_metadata_client = EC2Metadata(http_client=http_client, config=config)
194
+ self._config = config or Config()
195
+ self._credentials = None
196
+ self._profile_name = self._config.ec2_instance_profile_name
197
+
198
+ async def get_identity(
199
+ self, *, identity_properties: IdentityProperties
200
+ ) -> AWSCredentialsIdentity:
201
+ if (
202
+ self._credentials is not None
203
+ and self._credentials.expiration
204
+ and datetime.now(UTC) < self._credentials.expiration
205
+ ):
206
+ return self._credentials
207
+
208
+ profile = self._profile_name
209
+ if profile is None:
210
+ profile = await self._ec2_metadata_client.get(path=self._METADATA_PATH_BASE)
211
+
212
+ creds_str = await self._ec2_metadata_client.get(
213
+ path=f"{self._METADATA_PATH_BASE}/{profile}"
214
+ )
215
+ creds = json.loads(creds_str)
216
+
217
+ access_key_id = creds.get("AccessKeyId")
218
+ secret_access_key = creds.get("SecretAccessKey")
219
+ session_token = creds.get("Token")
220
+ account_id = creds.get("AccountId")
221
+ expiration = creds.get("Expiration")
222
+ if expiration is not None:
223
+ expiration = datetime.fromisoformat(expiration).replace(tzinfo=UTC)
224
+
225
+ if access_key_id is None or secret_access_key is None:
226
+ raise SmithyIdentityException(
227
+ "AccessKeyId and SecretAccessKey are required"
228
+ )
229
+
230
+ self._credentials = AWSCredentialsIdentity(
231
+ access_key_id=access_key_id,
232
+ secret_access_key=secret_access_key,
233
+ session_token=session_token,
234
+ expiration=expiration,
235
+ account_id=account_id,
236
+ )
237
+ return self._credentials
@@ -0,0 +1,20 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ from smithy_core.aio.interfaces.identity import IdentityResolver
4
+ from smithy_core.interfaces.identity import IdentityProperties
5
+
6
+ from smithy_aws_core.identity import AWSCredentialsIdentity
7
+
8
+
9
+ class StaticCredentialsResolver(
10
+ IdentityResolver[AWSCredentialsIdentity, IdentityProperties]
11
+ ):
12
+ """Resolve Static AWS Credentials."""
13
+
14
+ def __init__(self, *, credentials: AWSCredentialsIdentity) -> None:
15
+ self._credentials = credentials
16
+
17
+ async def get_identity(
18
+ self, *, identity_properties: IdentityProperties
19
+ ) -> AWSCredentialsIdentity:
20
+ return self._credentials
@@ -0,0 +1,15 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ from smithy_core.endpoints import StaticEndpointConfig
4
+ from smithy_core.types import PropertyKey
5
+
6
+
7
+ class RegionalEndpointConfig(StaticEndpointConfig):
8
+ """Endpoint config for services with standard regional endpoints."""
9
+
10
+ region: str | None
11
+ """The AWS region to address the request to."""
12
+
13
+
14
+ REGIONAL_ENDPOINT_CONFIG = PropertyKey(key="config", value_type=RegionalEndpointConfig)
15
+ """Endpoint config for services with standard regional endpoints."""
@@ -0,0 +1,33 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ from typing import Any
4
+
5
+ from smithy_core import URI
6
+ from smithy_core.aio.interfaces import EndpointResolver
7
+ from smithy_core.endpoints import Endpoint, EndpointResolverParams, resolve_static_uri
8
+ from smithy_core.exceptions import EndpointResolutionError
9
+
10
+ from . import REGIONAL_ENDPOINT_CONFIG
11
+
12
+
13
+ class StandardRegionalEndpointsResolver(EndpointResolver):
14
+ """Resolves endpoints for services with standard regional endpoints."""
15
+
16
+ def __init__(self, endpoint_prefix: str = "bedrock-runtime"):
17
+ self._endpoint_prefix = endpoint_prefix
18
+
19
+ async def resolve_endpoint(self, params: EndpointResolverParams[Any]) -> Endpoint:
20
+ if (static_uri := resolve_static_uri(params)) is not None:
21
+ return Endpoint(uri=static_uri)
22
+
23
+ region_config = params.context.get(REGIONAL_ENDPOINT_CONFIG)
24
+ if region_config is not None and region_config.region is not None:
25
+ # TODO: use dns suffix determined from partition metadata
26
+ dns_suffix = "amazonaws.com"
27
+ hostname = f"{self._endpoint_prefix}.{region_config.region}.{dns_suffix}"
28
+
29
+ return Endpoint(uri=URI(host=hostname))
30
+
31
+ raise EndpointResolutionError(
32
+ "Unable to resolve endpoint - either endpoint_url or region are required."
33
+ )
@@ -0,0 +1,68 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License"). You
4
+ # may not use this file except in compliance with the License. A copy of
5
+ # the License is located at
6
+ #
7
+ # http://aws.amazon.com/apache2.0/
8
+ #
9
+ # or in the "license" file accompanying this file. This file is
10
+ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
+ # ANY KIND, either express or implied. See the License for the specific
12
+ # language governing permissions and limitations under the License.
13
+ from datetime import datetime
14
+
15
+ from smithy_core.aio.interfaces.identity import IdentityResolver
16
+ from smithy_core.identity import Identity
17
+ from smithy_core.interfaces.identity import IdentityProperties
18
+
19
+
20
+ class AWSCredentialsIdentity(Identity):
21
+ """Container for AWS authentication credentials."""
22
+
23
+ def __init__(
24
+ self,
25
+ *,
26
+ access_key_id: str,
27
+ secret_access_key: str,
28
+ session_token: str | None = None,
29
+ expiration: datetime | None = None,
30
+ account_id: str | None = None,
31
+ ) -> None:
32
+ """Initialize the AWSCredentialIdentity.
33
+
34
+ :param access_key_id: A unique identifier for an AWS user or role.
35
+ :param secret_access_key: A secret key used in conjunction with the access key
36
+ ID to authenticate programmatic access to AWS services.
37
+ :param session_token: A temporary token used to specify the current session for
38
+ the supplied credentials.
39
+ :param expiration: The expiration time of the identity. If time zone is
40
+ provided, it is updated to UTC. The value must always be in UTC.
41
+ :param account_id: The AWS account's ID.
42
+ """
43
+ super().__init__(expiration=expiration)
44
+ self._access_key_id: str = access_key_id
45
+ self._secret_access_key: str = secret_access_key
46
+ self._session_token: str | None = session_token
47
+ self._account_id: str | None = account_id
48
+
49
+ @property
50
+ def access_key_id(self) -> str:
51
+ return self._access_key_id
52
+
53
+ @property
54
+ def secret_access_key(self) -> str:
55
+ return self._secret_access_key
56
+
57
+ @property
58
+ def session_token(self) -> str | None:
59
+ return self._session_token
60
+
61
+ @property
62
+ def account_id(self) -> str | None:
63
+ return self._account_id
64
+
65
+
66
+ type AWSCredentialsResolver = IdentityResolver[
67
+ AWSCredentialsIdentity, IdentityProperties
68
+ ]
@@ -0,0 +1,2 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,72 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ # pyright: reportMissingTypeStubs=false
4
+ from typing import Any
5
+
6
+ import smithy_core
7
+ from smithy_core.interceptors import Interceptor, RequestContext
8
+ from smithy_http.interceptors.user_agent import USER_AGENT
9
+ from smithy_http.user_agent import RawStringUserAgentComponent, UserAgentComponent
10
+
11
+ from .. import __version__
12
+
13
+ _USERAGENT_SDK_NAME = "aws-sdk-python"
14
+
15
+
16
+ class UserAgentInterceptor(Interceptor[Any, Any, Any, Any]):
17
+ """Adds AWS fields to the UserAgent."""
18
+
19
+ def __init__(
20
+ self,
21
+ *,
22
+ ua_suffix: str | None,
23
+ ua_app_id: str | None,
24
+ sdk_version: str,
25
+ service_id: str,
26
+ ) -> None:
27
+ """Initialize the UserAgentInterceptor.
28
+
29
+ :param ua_suffix: Additional suffix to be added to the UserAgent header.
30
+ :param ua_app_id: User defined and opaque application ID to be added to the
31
+ UserAgent header.
32
+ :param sdk_version: SDK version to be added to the UserAgent header.
33
+ :param service_id: ServiceId to be added to the UserAgent header.
34
+ """
35
+ super().__init__()
36
+ self._ua_suffix = ua_suffix
37
+ self._ua_app_id = ua_app_id
38
+ self._sdk_version = sdk_version
39
+ self._service_id = service_id
40
+
41
+ def read_after_serialization(self, context: RequestContext[Any, Any]) -> None:
42
+ if USER_AGENT in context.properties:
43
+ user_agent = context.properties[USER_AGENT]
44
+ user_agent.sdk_metadata = self._build_sdk_metadata()
45
+ user_agent.api_metadata.append(
46
+ UserAgentComponent("api", self._service_id, self._sdk_version)
47
+ )
48
+
49
+ if self._ua_app_id is not None:
50
+ user_agent.additional_metadata.append(
51
+ UserAgentComponent("app", self._ua_app_id)
52
+ )
53
+
54
+ if self._ua_suffix is not None:
55
+ user_agent.additional_metadata.append(
56
+ RawStringUserAgentComponent(self._ua_suffix)
57
+ )
58
+
59
+ def _build_sdk_metadata(self) -> list[UserAgentComponent]:
60
+ return [
61
+ UserAgentComponent(_USERAGENT_SDK_NAME, __version__),
62
+ UserAgentComponent("md", "smithy-core", smithy_core.__version__),
63
+ *self._crt_version(),
64
+ ]
65
+
66
+ def _crt_version(self) -> list[UserAgentComponent]:
67
+ try:
68
+ import awscrt
69
+
70
+ return [UserAgentComponent("md", "awscrt", awscrt.__version__)]
71
+ except (ImportError, AttributeError):
72
+ return []
@@ -0,0 +1 @@
1
+ Marker
@@ -0,0 +1,44 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ # This ruff check warns against using the assert statement, which can be stripped out
5
+ # when running Python with certain (common) optimization settings. Assert is used here
6
+ # for trait values. Since these are always generated, we can be fairly confident that
7
+ # they're correct regardless, so it's okay if the checks are stripped out.
8
+ # ruff: noqa: S101
9
+
10
+ from collections.abc import Mapping, Sequence
11
+ from dataclasses import dataclass, field
12
+
13
+ from smithy_core.shapes import ShapeID
14
+ from smithy_core.traits import DocumentValue, DynamicTrait, Trait
15
+
16
+
17
+ @dataclass(init=False, frozen=True)
18
+ class RestJson1Trait(Trait, id=ShapeID("aws.protocols#restJson1")):
19
+ http: Sequence[str] = field(
20
+ repr=False, hash=False, compare=False, default_factory=tuple
21
+ )
22
+ event_stream_http: Sequence[str] = field(
23
+ repr=False, hash=False, compare=False, default_factory=tuple
24
+ )
25
+
26
+ def __init__(self, value: DocumentValue | DynamicTrait = None):
27
+ super().__init__(value)
28
+ assert isinstance(self.document_value, Mapping)
29
+
30
+ http_versions = self.document_value["http"]
31
+ assert isinstance(http_versions, Sequence)
32
+ for val in http_versions:
33
+ assert isinstance(val, str)
34
+ object.__setattr__(self, "http", tuple(http_versions))
35
+ event_stream_http_versions = self.document_value.get("eventStreamHttp")
36
+ if not event_stream_http_versions:
37
+ object.__setattr__(self, "event_stream_http", self.http)
38
+ else:
39
+ assert isinstance(event_stream_http_versions, Sequence)
40
+ for val in event_stream_http_versions:
41
+ assert isinstance(val, str)
42
+ object.__setattr__(
43
+ self, "event_stream_http", tuple(event_stream_http_versions)
44
+ )