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.
- standard_aws-0.2.0/.gitignore +31 -0
- standard_aws-0.2.0/LICENSE +21 -0
- standard_aws-0.2.0/PKG-INFO +156 -0
- standard_aws-0.2.0/README.md +131 -0
- standard_aws-0.2.0/pyproject.toml +42 -0
- standard_aws-0.2.0/src/std/aws/__init__.py +134 -0
- standard_aws-0.2.0/src/std/aws/_client.py +45 -0
- standard_aws-0.2.0/src/std/aws/auth/__init__.py +19 -0
- standard_aws-0.2.0/src/std/aws/auth/credentials.py +169 -0
- standard_aws-0.2.0/src/std/aws/dynamodb/__init__.py +5 -0
- standard_aws-0.2.0/src/std/aws/dynamodb/table.py +467 -0
- standard_aws-0.2.0/src/std/aws/s3/__init__.py +5 -0
- standard_aws-0.2.0/src/std/aws/s3/client.py +305 -0
- standard_aws-0.2.0/src/std/aws/sqs/__init__.py +6 -0
- standard_aws-0.2.0/src/std/aws/sqs/client.py +291 -0
- standard_aws-0.2.0/src/std/aws/sqs/consumer.py +103 -0
|
@@ -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
|
+
]
|