standard-aws 0.2.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.
@@ -0,0 +1,156 @@
1
+ Metadata-Version: 2.5
2
+ Name: standard-aws
3
+ Version: 0.2.0
4
+ Summary: Async AWS clients and credential providers for Standard
5
+ Project-URL: Documentation, https://appunni-m.github.io/python-standard/api/
6
+ Project-URL: Repository, https://github.com/appunni-m/python-standard
7
+ Author: Standard contributors
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: aws,boto3,pydantic-settings,standard
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: aioboto3<16,>=13
23
+ Requires-Dist: standard==0.2.0
24
+ Description-Content-Type: text/markdown
25
+
26
+ # standard-aws
27
+
28
+ `standard-aws` adds asynchronous AWS clients to the `standard` Python library.
29
+ Install it on its own or select it with the core distribution's extra:
30
+
31
+ ```console
32
+ python -m pip install standard-aws
33
+ python -m pip install 'standard[aws]'
34
+ ```
35
+
36
+ The public import namespace is `std.aws`. The core `standard` package stays
37
+ small; AWS SDK dependencies are installed only with this add-on.
38
+
39
+ ## Credentials
40
+
41
+ Choose a credential provider explicitly when the application needs a fixed
42
+ source. The default provider delegates to the AWS SDK chain, so environment,
43
+ shared profiles, SSO, web identity, and workload roles keep their SDK behavior.
44
+
45
+ ```python
46
+ from std.aws import DefaultCredentialProvider, ExplicitCredentialProvider
47
+
48
+ credentials = DefaultCredentialProvider()
49
+ # Or provide short-lived credentials supplied by an application identity system.
50
+ fixed_credentials = ExplicitCredentialProvider(
51
+ access_key_id="access-key",
52
+ secret_access_key="secret-key",
53
+ session_token="session-token",
54
+ region="us-east-1",
55
+ )
56
+ ```
57
+
58
+ Credential objects hide secret values from their `repr`. Avoid logging values
59
+ returned by `to_dict()` because that method is intended for SDK integration.
60
+
61
+ ## S3
62
+
63
+ ```python
64
+ from std.aws import S3Client, S3ClientConfig
65
+
66
+ s3 = S3Client(S3ClientConfig(bucket_name="reports", region="us-east-1"))
67
+ await s3.put_object("daily/report.json", '{"ready": true}', content_type="application/json")
68
+ report = await s3.get_object_as_json("daily/report.json")
69
+ keys = await s3.list_objects(prefix="daily/")
70
+ ```
71
+
72
+ `S3Client` also supports file and file-object upload/download, object
73
+ responses, bytes and text reads, Pydantic model validation, copy, deletion,
74
+ presigned GET/PUT/DELETE URLs, and LocalStack or S3-compatible endpoints. S3
75
+ body streams are closed after helper reads. `get_object()` returns the response
76
+ with its body buffered in memory; use `async with s3.open_object(key)` to stream
77
+ large objects while keeping the SDK client open. `list_objects()` accepts
78
+ between 1 and 1000 keys per call, matching S3's service limit.
79
+
80
+ ## SQS
81
+
82
+ ```python
83
+ from std.aws import SQSClient, SQSConfig
84
+
85
+ queue = SQSClient(SQSConfig(queue_url="https://sqs.us-east-1.amazonaws.com/123/tasks"))
86
+ await queue.send_message({"task_id": "task-123"})
87
+ messages = await queue.receive_message(max_number_of_messages=10, wait_time_seconds=10)
88
+ ```
89
+
90
+ Mappings and Pydantic models are JSON encoded. Batch operations enforce SQS's
91
+ 1–10 message limit and reject mismatched per-message attributes. `process_messages`
92
+ supports synchronous and asynchronous handlers; synchronous handlers run outside
93
+ the event loop. Successful messages are deleted only when `auto_delete=True`.
94
+ Failed JSON model parsing leaves the original body intact and adds
95
+ `_parse_error` to the message.
96
+
97
+ For a continuously polling service, use the lifecycle-aware consumer:
98
+
99
+ ```python
100
+ from std.aws import SQSConsumer
101
+
102
+
103
+ async def handle(message):
104
+ print(message["Body"])
105
+
106
+
107
+ consumer = SQSConsumer(queue_url="https://…", region="us-east-1", handler=handle)
108
+ await consumer.start() # starts its managed worker and returns
109
+ await consumer.stop() # signals graceful shutdown and waits for completion
110
+ ```
111
+
112
+ `SQSConsumer` is a `std.service.BackgroundService`, so its state and health can
113
+ be managed alongside application services.
114
+
115
+ ## DynamoDB
116
+
117
+ ```python
118
+ from pydantic import BaseModel
119
+ from std.aws import DynamoDBConfig, DynamoTable
120
+
121
+
122
+ class Task(BaseModel):
123
+ task_id: str
124
+ title: str
125
+
126
+
127
+ tasks = DynamoTable(DynamoDBConfig(table_name="tasks"), Task)
128
+ await tasks.put_item(Task(task_id="task-123", title="Build docs"))
129
+ task = await tasks.get_item({"task_id": "task-123"})
130
+ ```
131
+
132
+ The typed helper supports create/delete table, get/put/update/delete item,
133
+ query/scan pagination, and batch read/write. Batch reads and writes are split at
134
+ AWS service limits and retry unprocessed keys or writes a bounded number of
135
+ times. Number attributes deserialize as `Decimal` to preserve precision; sets,
136
+ binary values, nested mappings, lists, booleans, and `None` are supported.
137
+
138
+ ## Secrets Manager settings source
139
+
140
+ `load_secret()` reads a JSON object from Secrets Manager. Its optional
141
+ `AwsSecretsManagerSettingsSource` adapts that mapping to Pydantic Settings.
142
+ Secret payloads must be JSON objects with unique keys and standard finite JSON
143
+ numbers.
144
+
145
+ ## Testing
146
+
147
+ Unit tests use fake SDK clients and do not need AWS credentials. The LocalStack
148
+ end-to-end test runs through Testcontainers:
149
+
150
+ ```console
151
+ make test-e2e
152
+ ```
153
+
154
+ The E2E test skips when Docker is unavailable. Set `STD_REQUIRE_DOCKER=1` to
155
+ make a missing Docker daemon a test failure. See the [AWS API guide](../../docs/aws.md)
156
+ for configuration details and edge-case behavior.
@@ -0,0 +1,15 @@
1
+ std/aws/__init__.py,sha256=YUeirBJJS8o0DCrkX1i6oXZCoy38z4YAQjVBUys5csg,4592
2
+ std/aws/_client.py,sha256=TuAsW1Ld7MzWq0xJCLcgdP5gNJocyMt2jGMigPYM3WM,1513
3
+ std/aws/auth/__init__.py,sha256=ne29GcFk7gLN2s9Ot6SfnY2dhP544wnTmtCuyGzZS90,456
4
+ std/aws/auth/credentials.py,sha256=6FjDrx8I52j5lTrMYE9fq7pu6UIZ5qOLjKWYAoKwQfo,6629
5
+ std/aws/dynamodb/__init__.py,sha256=7OpybCCjDQm0jOWBXyrgVuGkD3zYkux2DhyxSI8z8MY,132
6
+ std/aws/dynamodb/table.py,sha256=mIX9041wWZqxkgYoskL9XSyn4wfRroBWdmg-mke61vk,20602
7
+ std/aws/s3/__init__.py,sha256=MIINmPYp_bTZNRXeVBPfZNq404iiQlp1rrUB_89H22w,122
8
+ std/aws/s3/client.py,sha256=_rFWzZpkoyRcin_KVqNqU6YT6omCUPgwFDKWpu6eJao,12310
9
+ std/aws/sqs/__init__.py,sha256=E244DPRphR_0p3PZOwV7B9WyFrAzuYkcsoorBPLna1g,254
10
+ std/aws/sqs/client.py,sha256=UxhA7_uYCMsuBgR_jrIkNTKPCJ-reMOZwokhBMDBhic,11782
11
+ std/aws/sqs/consumer.py,sha256=IstuyoseAc3Yq8iBovtyb2fuK5yz116zAwUMEWUqHus,3980
12
+ standard_aws-0.2.0.dist-info/METADATA,sha256=ia-K1cgRIyOfMgPS66pZSfSl4fnD1kvkYuJl7LsiHZY,5665
13
+ standard_aws-0.2.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
14
+ standard_aws-0.2.0.dist-info/licenses/LICENSE,sha256=sTIUP5MvMnfCSD0TdNTwjyFwBdAsnTBG2PfgodF7c9k,1078
15
+ standard_aws-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Standard contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
std/aws/__init__.py ADDED
@@ -0,0 +1,134 @@
1
+ """Asynchronous AWS integrations for Standard applications."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from collections.abc import Mapping
7
+ from importlib import import_module
8
+ from typing import Any, NoReturn
9
+
10
+ from pydantic_settings import BaseSettings, PydanticBaseSettingsSource
11
+
12
+ from .auth.credentials import (
13
+ AWSCredentials,
14
+ CredentialProvider,
15
+ DefaultCredentialProvider,
16
+ EnvironmentCredentialProvider,
17
+ ExplicitCredentialProvider,
18
+ ProfileCredentialProvider,
19
+ )
20
+ from .dynamodb.table import DynamoDBConfig, DynamoTable
21
+ from .s3.client import S3Client, S3ClientConfig
22
+ from .sqs.client import SQSClient, SQSConfig
23
+ from .sqs.consumer import SQSConsumer
24
+
25
+
26
+ class AwsSecretError(RuntimeError):
27
+ """A secret could not be retrieved or did not contain a JSON object."""
28
+
29
+
30
+ def _unique_json_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
31
+ """Build a JSON object while rejecting ambiguous duplicate keys."""
32
+ result: dict[str, Any] = {}
33
+ for key, value in pairs:
34
+ if key in result:
35
+ raise ValueError("duplicate JSON object key")
36
+ result[key] = value
37
+ return result
38
+
39
+
40
+ def _reject_non_finite_number(value: str) -> NoReturn:
41
+ """Reject Python's non-standard JSON NaN and Infinity extensions."""
42
+ raise ValueError(f"non-standard JSON numeric constant: {value}")
43
+
44
+
45
+ def load_secret(
46
+ secret_id: str,
47
+ *,
48
+ region_name: str | None = None,
49
+ client: Any | None = None,
50
+ ) -> dict[str, Any]:
51
+ """Load a JSON object from AWS Secrets Manager without exposing secret data."""
52
+ if not secret_id.strip():
53
+ raise ValueError("secret_id must not be empty")
54
+ try:
55
+ if client is None:
56
+ try:
57
+ boto3: Any = import_module("boto3")
58
+ except ImportError as error: # pragma: no cover - installation dependent
59
+ raise ImportError(
60
+ "AWS Secrets Manager requires boto3; install `standard-aws` or `standard[aws]`."
61
+ ) from error
62
+ secrets_client = boto3.client("secretsmanager", region_name=region_name)
63
+ else:
64
+ secrets_client = client
65
+ response = secrets_client.get_secret_value(SecretId=secret_id)
66
+ if "SecretString" in response:
67
+ raw = response["SecretString"]
68
+ elif "SecretBinary" in response:
69
+ binary = response["SecretBinary"]
70
+ raw = (
71
+ binary.decode("utf-8")
72
+ if isinstance(binary, bytes)
73
+ else bytes(binary).decode("utf-8")
74
+ )
75
+ else:
76
+ raise AwsSecretError("AWS Secrets Manager returned no secret payload")
77
+ value = json.loads(
78
+ raw,
79
+ object_pairs_hook=_unique_json_object,
80
+ parse_constant=_reject_non_finite_number,
81
+ )
82
+ except (AwsSecretError, ImportError):
83
+ raise
84
+ except Exception:
85
+ raise AwsSecretError("AWS Secrets Manager request or JSON decoding failed") from None
86
+ if not isinstance(value, Mapping) or any(not isinstance(key, str) for key in value):
87
+ raise AwsSecretError("AWS secret payload must be a JSON object with string keys")
88
+ return dict(value)
89
+
90
+
91
+ class AwsSecretsManagerSettingsSource(PydanticBaseSettingsSource):
92
+ """Pydantic source that maps an AWS Secrets Manager JSON object to fields."""
93
+
94
+ def __init__(
95
+ self,
96
+ settings_cls: type[BaseSettings],
97
+ secret_id: str,
98
+ *,
99
+ region_name: str | None = None,
100
+ client: Any | None = None,
101
+ ) -> None:
102
+ """Configure the source with a Settings model and secret identifier."""
103
+ super().__init__(settings_cls)
104
+ self.secret_id = secret_id
105
+ self.region_name = region_name
106
+ self.client = client
107
+
108
+ def __call__(self) -> dict[str, object]:
109
+ """Return the secret object as a mapping of Settings field values."""
110
+ return load_secret(self.secret_id, region_name=self.region_name, client=self.client)
111
+
112
+ def get_field_value(self, field: object, field_name: str) -> tuple[object, str, bool]:
113
+ """Satisfy the Pydantic source protocol; this source returns a full mapping."""
114
+ return None, field_name, False
115
+
116
+
117
+ __all__ = [
118
+ "AWSCredentials",
119
+ "AwsSecretError",
120
+ "AwsSecretsManagerSettingsSource",
121
+ "CredentialProvider",
122
+ "DefaultCredentialProvider",
123
+ "DynamoDBConfig",
124
+ "DynamoTable",
125
+ "EnvironmentCredentialProvider",
126
+ "ExplicitCredentialProvider",
127
+ "ProfileCredentialProvider",
128
+ "S3Client",
129
+ "S3ClientConfig",
130
+ "SQSClient",
131
+ "SQSConfig",
132
+ "SQSConsumer",
133
+ "load_secret",
134
+ ]
std/aws/_client.py ADDED
@@ -0,0 +1,45 @@
1
+ """Shared creation of short-lived async AWS SDK clients."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import AsyncIterator
6
+ from contextlib import asynccontextmanager
7
+ from importlib import import_module
8
+ from typing import Any
9
+
10
+ from .auth.credentials import CredentialProvider
11
+
12
+
13
+ @asynccontextmanager
14
+ async def open_client(
15
+ service_name: str,
16
+ *,
17
+ region: str | None,
18
+ endpoint_url: str | None,
19
+ credential_provider: CredentialProvider,
20
+ ) -> AsyncIterator[Any]:
21
+ """Open an aioboto3 client with provider credentials and guaranteed cleanup."""
22
+ try:
23
+ aioboto3: Any = import_module("aioboto3")
24
+ except ImportError as error: # pragma: no cover - package installation dependent
25
+ raise ImportError("AWS clients require aioboto3; install `standard-aws`.") from error
26
+
27
+ session = aioboto3.Session()
28
+ kwargs: dict[str, Any] = {}
29
+ if endpoint_url is not None:
30
+ kwargs["endpoint_url"] = endpoint_url
31
+ credentials = await credential_provider.get_credentials()
32
+ credential_kwargs: dict[str, str] = {}
33
+ if credentials is not None:
34
+ credential_kwargs = credentials.to_dict()
35
+ credential_kwargs.pop("region_name", None)
36
+ kwargs["region_name"] = (
37
+ region
38
+ or (credentials.region if credentials is not None else None)
39
+ or getattr(session, "region_name", None)
40
+ or "us-east-1"
41
+ )
42
+ kwargs.update(credential_kwargs)
43
+
44
+ async with session.client(service_name, **kwargs) as client:
45
+ yield client
@@ -0,0 +1,19 @@
1
+ """Credential providers used by AWS integrations."""
2
+
3
+ from .credentials import (
4
+ AWSCredentials,
5
+ CredentialProvider,
6
+ DefaultCredentialProvider,
7
+ EnvironmentCredentialProvider,
8
+ ExplicitCredentialProvider,
9
+ ProfileCredentialProvider,
10
+ )
11
+
12
+ __all__ = [
13
+ "AWSCredentials",
14
+ "CredentialProvider",
15
+ "DefaultCredentialProvider",
16
+ "EnvironmentCredentialProvider",
17
+ "ExplicitCredentialProvider",
18
+ "ProfileCredentialProvider",
19
+ ]
@@ -0,0 +1,169 @@
1
+ """Async credential providers backed by explicit values and the AWS SDK chain."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import os
7
+ from abc import ABC, abstractmethod
8
+ from importlib import import_module
9
+ from typing import Any
10
+
11
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
12
+
13
+
14
+ class AWSCredentials(BaseModel):
15
+ """AWS access credentials with secret fields hidden from representations."""
16
+
17
+ model_config = ConfigDict(extra="forbid", frozen=True, str_strip_whitespace=True)
18
+
19
+ access_key_id: str = Field(min_length=1, description="AWS access key ID")
20
+ secret_access_key: str = Field(min_length=1, repr=False, description="AWS secret access key")
21
+ session_token: str | None = Field(default=None, repr=False)
22
+ region: str | None = None
23
+
24
+ @field_validator("session_token", "region")
25
+ @classmethod
26
+ def _reject_empty_optional_values(cls, value: str | None) -> str | None:
27
+ if value == "":
28
+ return None
29
+ return value
30
+
31
+ def to_dict(self) -> dict[str, str]:
32
+ """Return keyword arguments accepted by the AWS SDK client factory."""
33
+ values = {
34
+ "aws_access_key_id": self.access_key_id,
35
+ "aws_secret_access_key": self.secret_access_key,
36
+ }
37
+ if self.session_token is not None:
38
+ values["aws_session_token"] = self.session_token
39
+ if self.region is not None:
40
+ values["region_name"] = self.region
41
+ return values
42
+
43
+
44
+ class CredentialProvider(ABC):
45
+ """Interface for asynchronous AWS credential lookup."""
46
+
47
+ @abstractmethod
48
+ async def get_credentials(self) -> AWSCredentials | None:
49
+ """Return credentials, or ``None`` to use the SDK's default chain."""
50
+
51
+
52
+ class ExplicitCredentialProvider(CredentialProvider):
53
+ """Return credentials configured directly by the application."""
54
+
55
+ def __init__(
56
+ self,
57
+ access_key_id: str,
58
+ secret_access_key: str,
59
+ session_token: str | None = None,
60
+ region: str | None = None,
61
+ ) -> None:
62
+ """Validate and retain static credentials for this provider."""
63
+ self._credentials = AWSCredentials(
64
+ access_key_id=access_key_id,
65
+ secret_access_key=secret_access_key,
66
+ session_token=session_token,
67
+ region=region,
68
+ )
69
+
70
+ async def get_credentials(self) -> AWSCredentials:
71
+ """Return the configured credentials."""
72
+ return self._credentials
73
+
74
+
75
+ class EnvironmentCredentialProvider(CredentialProvider):
76
+ """Read credentials from the standard AWS environment variables."""
77
+
78
+ async def get_credentials(self) -> AWSCredentials | None:
79
+ """Return credentials only when both access-key variables are present."""
80
+ access_key_id = os.getenv("AWS_ACCESS_KEY_ID")
81
+ secret_access_key = os.getenv("AWS_SECRET_ACCESS_KEY")
82
+ if not access_key_id or not secret_access_key:
83
+ return None
84
+ return AWSCredentials(
85
+ access_key_id=access_key_id,
86
+ secret_access_key=secret_access_key,
87
+ session_token=os.getenv("AWS_SESSION_TOKEN"),
88
+ region=os.getenv("AWS_DEFAULT_REGION") or os.getenv("AWS_REGION"),
89
+ )
90
+
91
+
92
+ class ProfileCredentialProvider(CredentialProvider):
93
+ """Resolve one named profile through the AWS SDK shared configuration chain."""
94
+
95
+ def __init__(self, profile_name: str = "default") -> None:
96
+ """Select the profile name read by the AWS SDK."""
97
+ if not profile_name.strip():
98
+ raise ValueError("profile_name must not be empty")
99
+ self.profile_name = profile_name.strip()
100
+
101
+ async def get_credentials(self) -> AWSCredentials | None:
102
+ """Resolve profile credentials off the event loop, including SSO/process providers."""
103
+ return await asyncio.to_thread(self._resolve_credentials)
104
+
105
+ def _resolve_credentials(self) -> AWSCredentials | None:
106
+ try:
107
+ boto3: Any = import_module("boto3")
108
+ except ImportError as error: # pragma: no cover - package installation dependent
109
+ raise ImportError(
110
+ "AWS credential lookup requires boto3; install `standard-aws`."
111
+ ) from error
112
+ try:
113
+ session = boto3.Session(profile_name=self.profile_name)
114
+ credentials = session.get_credentials()
115
+ if credentials is None:
116
+ return None
117
+ frozen = credentials.get_frozen_credentials()
118
+ return AWSCredentials(
119
+ access_key_id=frozen.access_key,
120
+ secret_access_key=frozen.secret_key,
121
+ session_token=frozen.token,
122
+ region=session.region_name,
123
+ )
124
+ except Exception as error:
125
+ if type(error).__name__ == "ProfileNotFound":
126
+ return None
127
+ raise
128
+
129
+
130
+ class DefaultCredentialProvider(CredentialProvider):
131
+ """Resolve the AWS SDK default chain, optionally pinning a named profile."""
132
+
133
+ def __init__(self, profile_name: str | None = None) -> None:
134
+ """Use the normal AWS SDK chain unless a profile name is supplied."""
135
+ if profile_name is not None and not profile_name.strip():
136
+ raise ValueError("profile_name must not be empty")
137
+ self.profile_name = profile_name.strip() if profile_name is not None else None
138
+
139
+ async def get_credentials(self) -> AWSCredentials | None:
140
+ """Return current credentials from the SDK chain without blocking the event loop."""
141
+ return await asyncio.to_thread(self._resolve_credentials)
142
+
143
+ def _resolve_credentials(self) -> AWSCredentials | None:
144
+ try:
145
+ boto3: Any = import_module("boto3")
146
+ except ImportError as error: # pragma: no cover - package installation dependent
147
+ raise ImportError(
148
+ "AWS credential lookup requires boto3; install `standard-aws`."
149
+ ) from error
150
+ try:
151
+ session = (
152
+ boto3.Session(profile_name=self.profile_name)
153
+ if self.profile_name is not None
154
+ else boto3.Session()
155
+ )
156
+ credentials = session.get_credentials()
157
+ if credentials is None:
158
+ return None
159
+ frozen = credentials.get_frozen_credentials()
160
+ return AWSCredentials(
161
+ access_key_id=frozen.access_key,
162
+ secret_access_key=frozen.secret_key,
163
+ session_token=frozen.token,
164
+ region=session.region_name,
165
+ )
166
+ except Exception as error:
167
+ if type(error).__name__ == "ProfileNotFound":
168
+ return None
169
+ raise
@@ -0,0 +1,5 @@
1
+ """Typed DynamoDB table operations."""
2
+
3
+ from .table import DynamoDBConfig, DynamoTable
4
+
5
+ __all__ = ["DynamoDBConfig", "DynamoTable"]