standard-aws 0.2.0__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,31 @@
1
+ # Workspace-managed mounts.
2
+ /.agents/
3
+ /.aws/
4
+ /.codex/
5
+
6
+ # Python tooling and generated files.
7
+ /.venv/
8
+ /venv/
9
+ __pycache__/
10
+ *.py[cod]
11
+ *.egg-info/
12
+ /build/
13
+ /dist/
14
+ /.coverage
15
+ /.coverage.*
16
+ /htmlcov/
17
+ /deprecated/
18
+ /.pytest_cache/
19
+ /.mypy_cache/
20
+ /.ruff_cache/
21
+ /.tox/
22
+
23
+ # Documentation output.
24
+ /.cache/
25
+ /site/
26
+
27
+ # Local secrets and environment files.
28
+ .env
29
+ .env.*
30
+ !.env.example
31
+ .kata.local.toml
@@ -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.
@@ -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,131 @@
1
+ # standard-aws
2
+
3
+ `standard-aws` adds asynchronous AWS clients to the `standard` Python library.
4
+ Install it on its own or select it with the core distribution's extra:
5
+
6
+ ```console
7
+ python -m pip install standard-aws
8
+ python -m pip install 'standard[aws]'
9
+ ```
10
+
11
+ The public import namespace is `std.aws`. The core `standard` package stays
12
+ small; AWS SDK dependencies are installed only with this add-on.
13
+
14
+ ## Credentials
15
+
16
+ Choose a credential provider explicitly when the application needs a fixed
17
+ source. The default provider delegates to the AWS SDK chain, so environment,
18
+ shared profiles, SSO, web identity, and workload roles keep their SDK behavior.
19
+
20
+ ```python
21
+ from std.aws import DefaultCredentialProvider, ExplicitCredentialProvider
22
+
23
+ credentials = DefaultCredentialProvider()
24
+ # Or provide short-lived credentials supplied by an application identity system.
25
+ fixed_credentials = ExplicitCredentialProvider(
26
+ access_key_id="access-key",
27
+ secret_access_key="secret-key",
28
+ session_token="session-token",
29
+ region="us-east-1",
30
+ )
31
+ ```
32
+
33
+ Credential objects hide secret values from their `repr`. Avoid logging values
34
+ returned by `to_dict()` because that method is intended for SDK integration.
35
+
36
+ ## S3
37
+
38
+ ```python
39
+ from std.aws import S3Client, S3ClientConfig
40
+
41
+ s3 = S3Client(S3ClientConfig(bucket_name="reports", region="us-east-1"))
42
+ await s3.put_object("daily/report.json", '{"ready": true}', content_type="application/json")
43
+ report = await s3.get_object_as_json("daily/report.json")
44
+ keys = await s3.list_objects(prefix="daily/")
45
+ ```
46
+
47
+ `S3Client` also supports file and file-object upload/download, object
48
+ responses, bytes and text reads, Pydantic model validation, copy, deletion,
49
+ presigned GET/PUT/DELETE URLs, and LocalStack or S3-compatible endpoints. S3
50
+ body streams are closed after helper reads. `get_object()` returns the response
51
+ with its body buffered in memory; use `async with s3.open_object(key)` to stream
52
+ large objects while keeping the SDK client open. `list_objects()` accepts
53
+ between 1 and 1000 keys per call, matching S3's service limit.
54
+
55
+ ## SQS
56
+
57
+ ```python
58
+ from std.aws import SQSClient, SQSConfig
59
+
60
+ queue = SQSClient(SQSConfig(queue_url="https://sqs.us-east-1.amazonaws.com/123/tasks"))
61
+ await queue.send_message({"task_id": "task-123"})
62
+ messages = await queue.receive_message(max_number_of_messages=10, wait_time_seconds=10)
63
+ ```
64
+
65
+ Mappings and Pydantic models are JSON encoded. Batch operations enforce SQS's
66
+ 1–10 message limit and reject mismatched per-message attributes. `process_messages`
67
+ supports synchronous and asynchronous handlers; synchronous handlers run outside
68
+ the event loop. Successful messages are deleted only when `auto_delete=True`.
69
+ Failed JSON model parsing leaves the original body intact and adds
70
+ `_parse_error` to the message.
71
+
72
+ For a continuously polling service, use the lifecycle-aware consumer:
73
+
74
+ ```python
75
+ from std.aws import SQSConsumer
76
+
77
+
78
+ async def handle(message):
79
+ print(message["Body"])
80
+
81
+
82
+ consumer = SQSConsumer(queue_url="https://…", region="us-east-1", handler=handle)
83
+ await consumer.start() # starts its managed worker and returns
84
+ await consumer.stop() # signals graceful shutdown and waits for completion
85
+ ```
86
+
87
+ `SQSConsumer` is a `std.service.BackgroundService`, so its state and health can
88
+ be managed alongside application services.
89
+
90
+ ## DynamoDB
91
+
92
+ ```python
93
+ from pydantic import BaseModel
94
+ from std.aws import DynamoDBConfig, DynamoTable
95
+
96
+
97
+ class Task(BaseModel):
98
+ task_id: str
99
+ title: str
100
+
101
+
102
+ tasks = DynamoTable(DynamoDBConfig(table_name="tasks"), Task)
103
+ await tasks.put_item(Task(task_id="task-123", title="Build docs"))
104
+ task = await tasks.get_item({"task_id": "task-123"})
105
+ ```
106
+
107
+ The typed helper supports create/delete table, get/put/update/delete item,
108
+ query/scan pagination, and batch read/write. Batch reads and writes are split at
109
+ AWS service limits and retry unprocessed keys or writes a bounded number of
110
+ times. Number attributes deserialize as `Decimal` to preserve precision; sets,
111
+ binary values, nested mappings, lists, booleans, and `None` are supported.
112
+
113
+ ## Secrets Manager settings source
114
+
115
+ `load_secret()` reads a JSON object from Secrets Manager. Its optional
116
+ `AwsSecretsManagerSettingsSource` adapts that mapping to Pydantic Settings.
117
+ Secret payloads must be JSON objects with unique keys and standard finite JSON
118
+ numbers.
119
+
120
+ ## Testing
121
+
122
+ Unit tests use fake SDK clients and do not need AWS credentials. The LocalStack
123
+ end-to-end test runs through Testcontainers:
124
+
125
+ ```console
126
+ make test-e2e
127
+ ```
128
+
129
+ The E2E test skips when Docker is unavailable. Set `STD_REQUIRE_DOCKER=1` to
130
+ make a missing Docker daemon a test failure. See the [AWS API guide](../../docs/aws.md)
131
+ for configuration details and edge-case behavior.
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27,<2", "hatch-vcs>=0.4,<1"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "standard-aws"
7
+ dynamic = ["version"]
8
+ description = "Async AWS clients and credential providers for Standard"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Standard contributors" }]
14
+ keywords = ["aws", "boto3", "pydantic-settings", "standard"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Programming Language :: Python :: 3.14",
25
+ "Typing :: Typed",
26
+ ]
27
+ dependencies = ["standard==0.2.0", "aioboto3>=13,<16"]
28
+ urls = { Documentation = "https://appunni-m.github.io/python-standard/api/", Repository = "https://github.com/appunni-m/python-standard" }
29
+
30
+ [tool.hatch.build.targets.wheel]
31
+ only-include = ["src/std/aws"]
32
+ sources = ["src"]
33
+
34
+ [tool.hatch.build.targets.sdist]
35
+ only-include = ["pyproject.toml", "README.md", "LICENSE", "src/std/aws"]
36
+
37
+ [tool.hatch.version]
38
+ source = "vcs"
39
+ tag-pattern = "^v(?P<version>\\d+\\.\\d+\\.\\d+(?:(?:a|b|rc)\\d+)?)$"
40
+
41
+ [tool.hatch.version.raw-options]
42
+ search_parent_directories = true
@@ -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
+ ]
@@ -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
+ ]