aws-simple
A clean, simple Python wrapper around AWS services (S3, Textract, Bedrock).
Features
- Simple API: Clean, intuitive interface without exposing Boto3 complexity
- Environment-based configuration: No credentials or config in code
- Custom endpoints: Works against LocalStack, MinIO or any S3-compatible stack
- Structured Textract output: Transforms AWS Blocks into clean, serializable JSON
- Type-safe: Fully typed with Python 3.10+ support
- Production-ready: Works with IAM roles, Docker, CI/CD pipelines
Installation
pip install aws-simple
Or install from source:
pip install -e .
Configuration
Configuration comes from environment variables (or from code — see Programmatic configuration):
# Required
export AWS_REGION=us-east-1
export AWS_S3_BUCKET=my-bucket-name
# Optional
export AWS_PROFILE=my-profile # For local development
export AWS_BEDROCK_MODEL_ID=anthropic.claude-3-5-sonnet-20241022-v2:0
export AWS_TEXTRACT_REGION=us-east-1
export AWS_BEDROCK_REGION=us-east-1
export AWS_SSL_VERIFY=true # SSL certificate verification (default: true, set to false to disable)
# Optional: custom endpoints (LocalStack, MinIO, any S3-compatible stack)
export AWS_ENDPOINT_URL=http://localhost:4566 # applies to every service
export AWS_S3_ENDPOINT_URL=http://localhost:9000 # per-service override
export AWS_TEXTRACT_ENDPOINT_URL=http://localhost:4566
export AWS_BEDROCK_ENDPOINT_URL=http://localhost:4566
# Optional: explicit credentials (see below)
export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_ACCESS_KEY=...
export AWS_SESSION_TOKEN=... # only for temporary credentials
Or use a .env file (see .env.example).
Custom endpoints (LocalStack / MinIO)
Set AWS_ENDPOINT_URL to point every client at an AWS-compatible stack, or a
per-service variable (AWS_S3_ENDPOINT_URL, AWS_TEXTRACT_ENDPOINT_URL,
AWS_BEDROCK_ENDPOINT_URL) to override it for a single service. Each
per-service variable falls back to AWS_ENDPOINT_URL, and when none is set the
regular AWS endpoints are used.
# Everything against LocalStack, S3 against a local MinIO
export AWS_ENDPOINT_URL=http://localhost:4566
export AWS_S3_ENDPOINT_URL=http://localhost:9000
export AWS_ACCESS_KEY_ID=test
export AWS_SECRET_ACCESS_KEY=test
If the local stack serves a self-signed certificate, AWS_SSL_VERIFY=false
disables verification. Never do that against a real AWS endpoint.
AWS Credentials
By default credentials are resolved by the standard boto3 chain, which is the recommended setup:
- IAM Role (recommended for production/EC2/ECS/Lambda)
- ~/.aws/credentials file, optionally selected with
AWS_PROFILE - The ambient environment
They can also be provided explicitly, which is what local stacks and applications juggling per-tenant or per-request credentials need:
| Variable | Notes |
|---|---|
AWS_ACCESS_KEY_ID |
optional |
AWS_SECRET_ACCESS_KEY |
optional |
AWS_SESSION_TOKEN |
optional, for temporary credentials |
AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are only handed to boto3 when
both are set — a partial pair is ignored, so it can never silently break the
default credential chain. AWS_SESSION_TOKEN is only sent alongside a complete
pair.
Credential values are never logged, never included in a Config repr, and are
scrubbed out of ClientInitializationError messages, so a boto3 error quoting a
key does not propagate it.
Programmatic configuration
Everything above can also be set from code, which is what an application that reads its settings from a config file, Parameter Store or CLI flags needs:
from aws_simple import configure
configure(
region="eu-west-3",
bucket="my-bucket",
endpoint_url="http://localhost:4566",
)
All arguments are optional and keyword-only. Arguments left out are untouched, so successive calls accumulate.
Precedence
From strongest to weakest:
configure()— values set from code- Environment variables —
AWS_REGION,AWS_S3_BUCKET, … .envfile — loaded at import time, and only for variables the environment does not already define- Built-in defaults — e.g.
us-east-1for the region
A setting you never pass to configure() keeps coming from the environment, so
you can override just the region and let everything else resolve as usual.
Settings
configure() argument |
Overrides |
|---|---|
region |
AWS_REGION |
profile |
AWS_PROFILE |
bucket |
AWS_S3_BUCKET |
endpoint_url |
AWS_ENDPOINT_URL |
s3_endpoint_url |
AWS_S3_ENDPOINT_URL |
textract_region |
AWS_TEXTRACT_REGION |
textract_endpoint_url |
AWS_TEXTRACT_ENDPOINT_URL |
bedrock_region |
AWS_BEDROCK_REGION |
bedrock_endpoint_url |
AWS_BEDROCK_ENDPOINT_URL |
bedrock_model_id |
AWS_BEDROCK_MODEL_ID |
aws_access_key_id |
AWS_ACCESS_KEY_ID |
aws_secret_access_key |
AWS_SECRET_ACCESS_KEY |
aws_session_token |
AWS_SESSION_TOKEN |
Credentials passed to configure() follow the same rules as their environment
counterparts: a partial pair is ignored, and the values are never logged, never
part of a Config repr, and are scrubbed out of client initialization errors.
Reconfiguring at runtime
AWS clients are built once and cached. Every configuration change through
configure() invalidates that cache, so it applies to the next call even when
a client was already built:
from aws_simple import configure, s3
s3.list_objects("a/") # client built for the current region
configure(region="eu-west-3") # cached clients dropped
s3.list_objects("b/") # rebuilt: hits eu-west-3
Mutating os.environ after the first call does not do this — the already
built client keeps its old settings. Use configure() instead.
reset_configuration() drops every value set from code, so configuration falls
back to the environment (this also invalidates the cache):
from aws_simple import reset_configuration
reset_configuration()
Per-call region overrides
Passing a region to an individual service call (s3.list_objects(..., region=...))
is out of scope: clients are cached per service, not per region, and a
per-call region would still leave the bucket, endpoint and credentials on the
global configuration. configure() is the supported way to change region — call
it when your application switches region, or once per region-scoped unit of
work.
Usage
S3 Operations
from aws_simple import s3
# Upload file
s3.upload_file("document.pdf", "docs/document.pdf")
# Download file
s3.download_file("docs/document.pdf", "/tmp/document.pdf")
# Read object as bytes
content = s3.read_object("docs/document.pdf")
# Write bytes (or a str, encoded as UTF-8) directly, no temp file needed
s3.put_object("docs/report.json", '{"status": "ok"}')
s3.put_object("docs/report.bin", b"\x00\x01\x02")
# Delete an object
s3.delete_object("docs/document.pdf")
# Copy an object (within a bucket, or across buckets)
s3.copy_object("inbox/document.pdf", "archive/document.pdf")
s3.copy_object(
"inbox/document.pdf",
"archive/document.pdf",
source_bucket="incoming",
dest_bucket="processed",
)
# List objects
files = s3.list_objects(prefix="docs/")
# Check if object exists
exists = s3.object_exists("docs/document.pdf")
All S3 functions take the bucket from the AWS_S3_BUCKET environment variable
unless one is passed explicitly, and raise S3Error (chaining the underlying
ClientError) with a message naming the s3://bucket/key involved.
Textract - Document Extraction
⚠️ Current limitation: 1 page per request
extract_text_from_file,extract_text_from_s3,extract_text_simple_from_file, andextract_text_simple_from_s3use the synchronous Textract APIs (analyze_document/detect_document_textwithDocument={"Bytes": ...}orDocument={"S3Object": ...}). These APIs reliably process only an image or a single-page PDF. For multi-page PDFs, only the first page is reliable (AWS does not guarantee behavior for subsequent pages).Supporting multi-page PDFs requires switching to the asynchronous Textract APIs (
StartDocumentAnalysis/StartDocumentTextDetectionvia S3) or splitting the document client-side before extraction — see #11.
from aws_simple import textract
import json
# Extract from local file (with tables)
doc = textract.extract_text_from_file("invoice.pdf")
# Extract from S3 (with tables)
doc = textract.extract_text_from_s3("docs/invoice.pdf")
# Access structured data
print(doc.full_text) # All text concatenated
print(f"Pages: {len(doc.pages)}")
# Access page details
page = doc.pages[0]
print(f"Lines: {len(page.lines)}")
print(f"Tables: {len(page.tables)}")
# Access lines
for line in page.lines:
print(f"{line.text} (confidence: {line.confidence})")
# Access tables
for table in page.tables:
print(f"Table: {table.rows}x{table.columns}")
print(table.cells) # 2D matrix of cell values
# Serialize to JSON
doc_json = doc.to_dict()
with open("result.json", "w") as f:
json.dump(doc_json, f, indent=2)
# Simple text extraction (faster, no tables)
text = textract.extract_text_simple_from_file("document.pdf")
Textract Output Format
The library transforms AWS Textract Blocks into a clean JSON structure:
{
"pages": [
{
"page_number": 1,
"width": 1.0,
"height": 1.0,
"lines": [
{
"text": "Invoice #12345",
"confidence": 99.5,
"bounding_box": {"top": 0.1, "left": 0.1, "width": 0.2, "height": 0.05}
}
],
"tables": [
{
"rows": 3,
"columns": 2,
"cells": [
["Item", "Price"],
["Product A", "$10"],
["Product B", "$20"]
],
"confidence": 98.7
}
],
"raw_text": "Invoice #12345\n..."
}
],
"full_text": "All text from all pages concatenated...",
"metadata": {
"document_metadata": {...},
"total_pages": 1
}
}
Bedrock - LLM Operations
Note: Supports Anthropic Claude, Amazon Titan, Meta Llama and Mistral models, selected automatically from the
model_id(e.g.anthropic.claude-3-5-sonnet-20241022-v2:0,amazon.titan-text-express-v1,meta.llama3-8b-instruct-v1:0,mistral.mistral-7b-instruct-v0:2). Callinginvoke()/invoke_json()with a model ID from another family raisesBedrockError.
from aws_simple import bedrock
# Simple text generation
response = bedrock.invoke("Explain AWS Lambda in one sentence")
print(response)
# With system prompt and parameters
response = bedrock.invoke(
prompt="What are the benefits of serverless?",
system_prompt="You are an AWS solutions architect.",
temperature=0.7,
max_tokens=500
)
# Request JSON output
prompt = """
List 3 AWS services with their use cases.
Format: {"services": [{"name": "...", "use_case": "..."}]}
"""
data = bedrock.invoke_json(prompt)
print(data["services"])
# Use different model
response = bedrock.invoke(
"Summarize this text...",
model_id="anthropic.claude-3-5-sonnet-20241022-v2:0"
)
Combined Workflow
from aws_simple import s3, textract, bedrock
import json
# 1. Upload document
s3.upload_file("invoice.pdf", "invoices/2024/inv_001.pdf")
# 2. Extract content
doc = textract.extract_text_from_s3("invoices/2024/inv_001.pdf")
# 3. Analyze with LLM
prompt = f"""
Extract key information from this invoice:
{doc.full_text}
Return JSON with: invoice_number, date, total, vendor
"""
invoice_data = bedrock.invoke_json(prompt)
print(json.dumps(invoice_data, indent=2))
Architecture
aws-simple/
├── config.py # Configuration: env vars, .env and configure()
├── exceptions.py # Custom exceptions
├── _clients.py # AWS client factory (internal)
├── s3.py # S3 operations
├── textract.py # Textract operations
├── bedrock.py # Bedrock operations
├── models/ # Data models
│ └── textract.py # TextractDocument, TextractPage, etc.
└── _parsers/ # Internal parsers
└── textract_parser.py # Transforms Blocks → JSON
Design Principles
- No Boto3 in public API: AWS implementation details are hidden
- Environment-based config: Env vars by default, overridable from code
with
configure() - Clean output formats: No raw AWS responses exposed
- Type safety: Full type hints for better IDE support
- Simple error handling: Custom exceptions for each service
- Production-ready: Compatible with Docker, IAM roles, CI/CD
Exceptions
from aws_simple import (
AWSSimpleError, # Base exception
ConfigurationError, # Missing/invalid configuration
S3Error, # S3 operation failures
TextractError, # Textract operation failures
BedrockError, # Bedrock operation failures
ClientInitializationError # AWS client init failures
)
try:
doc = textract.extract_text_from_s3("missing.pdf")
except TextractError as e:
print(f"Extraction failed: {e}")
Development
# Install with dev dependencies
pip install -e ".[dev]"
# Run tests
pytest
# Type checking
mypy src/
# Linting
ruff check src/
Integration tests (LocalStack)
The default pytest run is unit-only: every test mocks boto3, so it is fast
and needs no network. Those tests assert what the library calls, never that
the call works — a wrong parameter name or a bad pagination token would pass
them all.
The tests/integration/ suite closes that gap by running the same S3
functions against a real S3 implementation over HTTP. It is marked
integration and deselected by default, so it never slows down or breaks an
ordinary test run.
# Unit tests only (the default; no endpoint needed)
pytest
# Start an S3 endpoint, then run the integration suite
docker run --rm -d -p 4566:4566 -e SERVICES=s3 localstack/localstack:3
pytest -m integration # or: make test-integration
LocalStack is driven purely through the library's public configuration —
AWS_ENDPOINT_URL and the credential variables — so the suite exercises the
same code path as a real deployment. Nothing patches library internals.
What it covers: the list_objects ContinuationToken pagination path
past S3's 1000-key page size, max_keys trimming across that boundary,
object_exists on a missing key (the HeadObject 404 branch),
put_object / read_object round trips including UTF-8 and empty bodies,
upload_file / download_file, delete_object idempotence, and
copy_object within and across buckets — including keys that need URL
encoding, where a real endpoint rejects what a mock accepts.
Scoped to S3 on purpose: LocalStack's free tier does not meaningfully emulate Textract or Bedrock.
Environment variables the suite honours:
| Variable | Default | Purpose |
|---|---|---|
AWS_ENDPOINT_URL |
http://127.0.0.1:4566 |
Endpoint to test against; any S3-compatible endpoint works |
AWS_SIMPLE_INTEGRATION_TIMEOUT |
60 |
Seconds to wait for the endpoint to answer before giving up |
AWS_SIMPLE_INTEGRATION_REQUIRED |
unset | When truthy, an unreachable endpoint fails instead of skipping |
Without a reachable endpoint the suite skips itself, so pytest -m integration is harmless on a machine without LocalStack. CI sets
AWS_SIMPLE_INTEGRATION_REQUIRED=1 in its own standalone
Integration Tests (LocalStack) job, so a job whose container never came up
fails loudly instead of passing green with everything skipped.
Requirements
- Python ≥ 3.10
- boto3 ≥ 1.34.0
- python-dotenv ≥ 1.0.0
License
Apache-2.0 — see the LICENSE file for details.
Support
For issues and feature requests, please visit the GitHub repository.
Metadata
Release files for aws-simple 1.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| aws_simple-1.1.0.tar.gz | 60.7 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| aws_simple-1.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 88.4 kB
Release files / aws_simple-1.1.0.tar.gz
| Download URL | aws_simple-1.1.0.tar.gz |
|---|---|
| Size | 60.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c1a7d326d4d93fb34cdcfbb6797457a675f5d3f7ec89d4416ed944389dc83e5e
|
|
BLAKE2b-256 checksum How to use checksums |
1b4b453d8339058cf42d7c3bf2836dc9ff7ad308d0de4f20e22317dcc38efffd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / aws_simple-1.1.0-py3-none-any.whl
| Download URL | aws_simple-1.1.0-py3-none-any.whl |
|---|---|
| Size | 27.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
25cc8ca6c2e249b136eb93e6adf44760f5a51de10ad224d2e3cf2b26d5d06a0c
|
|
BLAKE2b-256 checksum How to use checksums |
cde45d49f81c4a30621ca0d4e1966c8e0dc6b2fdb7175529a9969b5f1df5513b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Security for aws-simple 1.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.