aws-simple 0.1.0b0__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.
- aws_simple/__init__.py +54 -0
- aws_simple/_clients.py +69 -0
- aws_simple/_parsers/__init__.py +5 -0
- aws_simple/_parsers/textract_parser.py +192 -0
- aws_simple/bedrock.py +169 -0
- aws_simple/config.py +71 -0
- aws_simple/exceptions.py +37 -0
- aws_simple/models/__init__.py +10 -0
- aws_simple/models/textract.py +82 -0
- aws_simple/s3.py +154 -0
- aws_simple/textract.py +179 -0
- aws_simple-0.1.0b0.dist-info/METADATA +309 -0
- aws_simple-0.1.0b0.dist-info/RECORD +15 -0
- aws_simple-0.1.0b0.dist-info/WHEEL +4 -0
- aws_simple-0.1.0b0.dist-info/licenses/LICENSE +201 -0
aws_simple/__init__.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
aws-simple: A clean, simple wrapper around AWS services.
|
|
3
|
+
|
|
4
|
+
Simplifies usage of AWS S3, Textract, and Bedrock through a clean API.
|
|
5
|
+
Configuration is done entirely via environment variables.
|
|
6
|
+
|
|
7
|
+
Example usage:
|
|
8
|
+
from aws_simple import s3, textract, bedrock
|
|
9
|
+
|
|
10
|
+
# S3 operations
|
|
11
|
+
s3.upload_file("doc.pdf", "docs/doc.pdf")
|
|
12
|
+
content = s3.read_object("docs/doc.pdf")
|
|
13
|
+
|
|
14
|
+
# Textract extraction
|
|
15
|
+
doc = textract.extract_text_from_s3("docs/doc.pdf")
|
|
16
|
+
print(doc.full_text)
|
|
17
|
+
print(doc.to_dict()) # Serialize to JSON
|
|
18
|
+
|
|
19
|
+
# Bedrock LLM
|
|
20
|
+
summary = bedrock.invoke("Summarize this document")
|
|
21
|
+
data = bedrock.invoke_json("Extract key points as JSON")
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from . import bedrock, s3, textract
|
|
25
|
+
from .exceptions import (
|
|
26
|
+
AWSSimpleError,
|
|
27
|
+
BedrockError,
|
|
28
|
+
ClientInitializationError,
|
|
29
|
+
ConfigurationError,
|
|
30
|
+
S3Error,
|
|
31
|
+
TextractError,
|
|
32
|
+
)
|
|
33
|
+
from .models import TextractDocument, TextractLine, TextractPage, TextractTable
|
|
34
|
+
|
|
35
|
+
__version__ = "0.1.0"
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
# Modules
|
|
39
|
+
"s3",
|
|
40
|
+
"textract",
|
|
41
|
+
"bedrock",
|
|
42
|
+
# Exceptions
|
|
43
|
+
"AWSSimpleError",
|
|
44
|
+
"BedrockError",
|
|
45
|
+
"ClientInitializationError",
|
|
46
|
+
"ConfigurationError",
|
|
47
|
+
"S3Error",
|
|
48
|
+
"TextractError",
|
|
49
|
+
# Models
|
|
50
|
+
"TextractDocument",
|
|
51
|
+
"TextractLine",
|
|
52
|
+
"TextractPage",
|
|
53
|
+
"TextractTable",
|
|
54
|
+
]
|
aws_simple/_clients.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Internal AWS clients factory (not exposed in public API)."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import boto3
|
|
6
|
+
from botocore.exceptions import BotoCoreError, ClientError, NoCredentialsError
|
|
7
|
+
|
|
8
|
+
from .config import config
|
|
9
|
+
from .exceptions import ClientInitializationError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AWSClients:
|
|
13
|
+
"""Factory for creating and caching AWS service clients."""
|
|
14
|
+
|
|
15
|
+
_s3_client: Any | None = None
|
|
16
|
+
_textract_client: Any | None = None
|
|
17
|
+
_bedrock_runtime_client: Any | None = None
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def _get_session_kwargs(cls) -> dict[str, str]:
|
|
21
|
+
"""Get boto3 session configuration."""
|
|
22
|
+
kwargs: dict[str, str] = {"region_name": config.aws_region}
|
|
23
|
+
if config.aws_profile:
|
|
24
|
+
kwargs["profile_name"] = config.aws_profile
|
|
25
|
+
return kwargs
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def get_s3_client(cls) -> Any:
|
|
29
|
+
"""Get or create S3 client."""
|
|
30
|
+
if cls._s3_client is None:
|
|
31
|
+
try:
|
|
32
|
+
session = boto3.Session(**cls._get_session_kwargs())
|
|
33
|
+
cls._s3_client = session.client("s3")
|
|
34
|
+
except (BotoCoreError, ClientError, NoCredentialsError) as e:
|
|
35
|
+
raise ClientInitializationError(f"Failed to initialize S3 client: {e}") from e
|
|
36
|
+
return cls._s3_client
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def get_textract_client(cls) -> Any:
|
|
40
|
+
"""Get or create Textract client."""
|
|
41
|
+
if cls._textract_client is None:
|
|
42
|
+
try:
|
|
43
|
+
kwargs = cls._get_session_kwargs()
|
|
44
|
+
kwargs["region_name"] = config.textract_region
|
|
45
|
+
session = boto3.Session(**kwargs)
|
|
46
|
+
cls._textract_client = session.client("textract")
|
|
47
|
+
except (BotoCoreError, ClientError, NoCredentialsError) as e:
|
|
48
|
+
raise ClientInitializationError(f"Failed to initialize Textract client: {e}") from e
|
|
49
|
+
return cls._textract_client
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def get_bedrock_runtime_client(cls) -> Any:
|
|
53
|
+
"""Get or create Bedrock Runtime client."""
|
|
54
|
+
if cls._bedrock_runtime_client is None:
|
|
55
|
+
try:
|
|
56
|
+
kwargs = cls._get_session_kwargs()
|
|
57
|
+
kwargs["region_name"] = config.bedrock_region
|
|
58
|
+
session = boto3.Session(**kwargs)
|
|
59
|
+
cls._bedrock_runtime_client = session.client("bedrock-runtime")
|
|
60
|
+
except (BotoCoreError, ClientError, NoCredentialsError) as e:
|
|
61
|
+
raise ClientInitializationError(f"Failed to initialize Bedrock client: {e}") from e
|
|
62
|
+
return cls._bedrock_runtime_client
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def reset_clients(cls) -> None:
|
|
66
|
+
"""Reset all cached clients (useful for testing)."""
|
|
67
|
+
cls._s3_client = None
|
|
68
|
+
cls._textract_client = None
|
|
69
|
+
cls._bedrock_runtime_client = None
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Parser to transform AWS Textract Blocks into clean JSON structure."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ..models.textract import (
|
|
6
|
+
TextractDocument,
|
|
7
|
+
TextractLine,
|
|
8
|
+
TextractPage,
|
|
9
|
+
TextractTable,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TextractParser:
|
|
14
|
+
"""Transforms AWS Textract response into clean, structured format."""
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def parse_response(response: dict[str, Any]) -> TextractDocument:
|
|
18
|
+
"""
|
|
19
|
+
Parse Textract API response into TextractDocument.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
response: Raw AWS Textract response with Blocks
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
TextractDocument with structured data
|
|
26
|
+
"""
|
|
27
|
+
blocks = response.get("Blocks", [])
|
|
28
|
+
|
|
29
|
+
# Build block lookup
|
|
30
|
+
block_map = {block["Id"]: block for block in blocks}
|
|
31
|
+
|
|
32
|
+
# Group blocks by page
|
|
33
|
+
pages_data: dict[int, dict[str, Any]] = {}
|
|
34
|
+
|
|
35
|
+
for block in blocks:
|
|
36
|
+
block_type = block.get("BlockType")
|
|
37
|
+
page_num = block.get("Page", 1)
|
|
38
|
+
|
|
39
|
+
if page_num not in pages_data:
|
|
40
|
+
pages_data[page_num] = {
|
|
41
|
+
"lines": [],
|
|
42
|
+
"tables": [],
|
|
43
|
+
"page_geometry": block.get("Geometry", {}),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if block_type == "LINE":
|
|
47
|
+
pages_data[page_num]["lines"].append(block)
|
|
48
|
+
elif block_type == "TABLE":
|
|
49
|
+
pages_data[page_num]["tables"].append(block)
|
|
50
|
+
|
|
51
|
+
# Build pages
|
|
52
|
+
pages: list[TextractPage] = []
|
|
53
|
+
all_text_parts: list[str] = []
|
|
54
|
+
|
|
55
|
+
for page_num in sorted(pages_data.keys()):
|
|
56
|
+
page_info = pages_data[page_num]
|
|
57
|
+
|
|
58
|
+
# Parse lines
|
|
59
|
+
lines = TextractParser._parse_lines(page_info["lines"])
|
|
60
|
+
|
|
61
|
+
# Parse tables
|
|
62
|
+
tables = TextractParser._parse_tables(page_info["tables"], block_map)
|
|
63
|
+
|
|
64
|
+
# Get page dimensions
|
|
65
|
+
geometry = page_info["page_geometry"]
|
|
66
|
+
bbox = geometry.get("BoundingBox", {})
|
|
67
|
+
width = bbox.get("Width", 1.0)
|
|
68
|
+
height = bbox.get("Height", 1.0)
|
|
69
|
+
|
|
70
|
+
# Concatenate text for this page
|
|
71
|
+
page_text = "\n".join(line.text for line in lines)
|
|
72
|
+
all_text_parts.append(page_text)
|
|
73
|
+
|
|
74
|
+
page = TextractPage(
|
|
75
|
+
page_number=page_num,
|
|
76
|
+
width=width,
|
|
77
|
+
height=height,
|
|
78
|
+
lines=lines,
|
|
79
|
+
tables=tables,
|
|
80
|
+
raw_text=page_text,
|
|
81
|
+
)
|
|
82
|
+
pages.append(page)
|
|
83
|
+
|
|
84
|
+
full_text = "\n\n".join(all_text_parts)
|
|
85
|
+
|
|
86
|
+
return TextractDocument(
|
|
87
|
+
pages=pages,
|
|
88
|
+
full_text=full_text,
|
|
89
|
+
metadata={
|
|
90
|
+
"document_metadata": response.get("DocumentMetadata", {}),
|
|
91
|
+
"total_pages": len(pages),
|
|
92
|
+
},
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
def _parse_lines(line_blocks: list[dict[str, Any]]) -> list[TextractLine]:
|
|
97
|
+
"""Parse LINE blocks into TextractLine objects."""
|
|
98
|
+
lines = []
|
|
99
|
+
for block in line_blocks:
|
|
100
|
+
text = block.get("Text", "")
|
|
101
|
+
confidence = block.get("Confidence", 0.0)
|
|
102
|
+
geometry = block.get("Geometry", {})
|
|
103
|
+
bbox = geometry.get("BoundingBox", {})
|
|
104
|
+
|
|
105
|
+
bounding_box = {
|
|
106
|
+
"top": bbox.get("Top", 0.0),
|
|
107
|
+
"left": bbox.get("Left", 0.0),
|
|
108
|
+
"width": bbox.get("Width", 0.0),
|
|
109
|
+
"height": bbox.get("Height", 0.0),
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
lines.append(
|
|
113
|
+
TextractLine(
|
|
114
|
+
text=text,
|
|
115
|
+
confidence=confidence,
|
|
116
|
+
bounding_box=bounding_box,
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
return lines
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def _parse_tables(
|
|
123
|
+
table_blocks: list[dict[str, Any]], block_map: dict[str, dict[str, Any]]
|
|
124
|
+
) -> list[TextractTable]:
|
|
125
|
+
"""Parse TABLE blocks into TextractTable objects."""
|
|
126
|
+
tables = []
|
|
127
|
+
|
|
128
|
+
for table_block in table_blocks:
|
|
129
|
+
# Get table dimensions
|
|
130
|
+
relationships = table_block.get("Relationships", [])
|
|
131
|
+
cell_ids = []
|
|
132
|
+
for rel in relationships:
|
|
133
|
+
if rel.get("Type") == "CHILD":
|
|
134
|
+
cell_ids = rel.get("Ids", [])
|
|
135
|
+
break
|
|
136
|
+
|
|
137
|
+
# Build cell matrix
|
|
138
|
+
cells_data: dict[tuple[int, int], str] = {}
|
|
139
|
+
max_row = 0
|
|
140
|
+
max_col = 0
|
|
141
|
+
|
|
142
|
+
for cell_id in cell_ids:
|
|
143
|
+
cell_block = block_map.get(cell_id)
|
|
144
|
+
if not cell_block or cell_block.get("BlockType") != "CELL":
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
row_index = cell_block.get("RowIndex", 1) - 1
|
|
148
|
+
col_index = cell_block.get("ColumnIndex", 1) - 1
|
|
149
|
+
max_row = max(max_row, row_index)
|
|
150
|
+
max_col = max(max_col, col_index)
|
|
151
|
+
|
|
152
|
+
# Get cell text from WORD children
|
|
153
|
+
cell_text = TextractParser._get_cell_text(cell_block, block_map)
|
|
154
|
+
cells_data[(row_index, col_index)] = cell_text
|
|
155
|
+
|
|
156
|
+
# Build matrix
|
|
157
|
+
rows = max_row + 1
|
|
158
|
+
cols = max_col + 1
|
|
159
|
+
cell_matrix = [["" for _ in range(cols)] for _ in range(rows)]
|
|
160
|
+
|
|
161
|
+
for (row_idx, col_idx), text in cells_data.items():
|
|
162
|
+
cell_matrix[row_idx][col_idx] = text
|
|
163
|
+
|
|
164
|
+
tables.append(
|
|
165
|
+
TextractTable(
|
|
166
|
+
rows=rows,
|
|
167
|
+
columns=cols,
|
|
168
|
+
cells=cell_matrix,
|
|
169
|
+
confidence=table_block.get("Confidence", 0.0),
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
return tables
|
|
174
|
+
|
|
175
|
+
@staticmethod
|
|
176
|
+
def _get_cell_text(cell_block: dict[str, Any], block_map: dict[str, dict[str, Any]]) -> str:
|
|
177
|
+
"""Extract text from a CELL block by following relationships."""
|
|
178
|
+
relationships = cell_block.get("Relationships", [])
|
|
179
|
+
word_ids = []
|
|
180
|
+
|
|
181
|
+
for rel in relationships:
|
|
182
|
+
if rel.get("Type") == "CHILD":
|
|
183
|
+
word_ids = rel.get("Ids", [])
|
|
184
|
+
break
|
|
185
|
+
|
|
186
|
+
words = []
|
|
187
|
+
for word_id in word_ids:
|
|
188
|
+
word_block = block_map.get(word_id)
|
|
189
|
+
if word_block and word_block.get("BlockType") == "WORD":
|
|
190
|
+
words.append(word_block.get("Text", ""))
|
|
191
|
+
|
|
192
|
+
return " ".join(words)
|
aws_simple/bedrock.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Bedrock operations module."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from botocore.exceptions import ClientError
|
|
7
|
+
|
|
8
|
+
from ._clients import AWSClients
|
|
9
|
+
from .config import config
|
|
10
|
+
from .exceptions import BedrockError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def invoke(
|
|
14
|
+
prompt: str,
|
|
15
|
+
model_id: str | None = None,
|
|
16
|
+
max_tokens: int = 4096,
|
|
17
|
+
temperature: float = 1.0,
|
|
18
|
+
system_prompt: str | None = None,
|
|
19
|
+
) -> str:
|
|
20
|
+
"""
|
|
21
|
+
Invoke Bedrock LLM and return text response.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
prompt: User prompt/question
|
|
25
|
+
model_id: Model ID (uses AWS_BEDROCK_MODEL_ID env var if not specified)
|
|
26
|
+
max_tokens: Maximum tokens to generate
|
|
27
|
+
temperature: Sampling temperature (0.0 to 1.0)
|
|
28
|
+
system_prompt: Optional system prompt
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Generated text response
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
BedrockError: If invocation fails
|
|
35
|
+
"""
|
|
36
|
+
model_id = model_id or config.bedrock_model_id
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
client = AWSClients.get_bedrock_runtime_client()
|
|
40
|
+
|
|
41
|
+
# Build request based on model family
|
|
42
|
+
if "anthropic.claude" in model_id:
|
|
43
|
+
body = _build_anthropic_request(
|
|
44
|
+
prompt=prompt,
|
|
45
|
+
max_tokens=max_tokens,
|
|
46
|
+
temperature=temperature,
|
|
47
|
+
system_prompt=system_prompt,
|
|
48
|
+
)
|
|
49
|
+
else:
|
|
50
|
+
raise BedrockError(
|
|
51
|
+
f"Unsupported model family: {model_id}. Currently only Claude models are supported."
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
response = client.invoke_model(
|
|
55
|
+
modelId=model_id,
|
|
56
|
+
body=json.dumps(body),
|
|
57
|
+
contentType="application/json",
|
|
58
|
+
accept="application/json",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
response_body = json.loads(response["body"].read())
|
|
62
|
+
|
|
63
|
+
# Extract text based on model family
|
|
64
|
+
if "anthropic.claude" in model_id:
|
|
65
|
+
return _extract_anthropic_text(response_body)
|
|
66
|
+
else:
|
|
67
|
+
raise BedrockError(f"Cannot extract response from model: {model_id}")
|
|
68
|
+
|
|
69
|
+
except ClientError as e:
|
|
70
|
+
raise BedrockError(f"Failed to invoke Bedrock model {model_id}: {e}") from e
|
|
71
|
+
except Exception as e:
|
|
72
|
+
raise BedrockError(f"Unexpected error invoking Bedrock: {e}") from e
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def invoke_json(
|
|
76
|
+
prompt: str,
|
|
77
|
+
model_id: str | None = None,
|
|
78
|
+
max_tokens: int = 4096,
|
|
79
|
+
temperature: float = 1.0,
|
|
80
|
+
system_prompt: str | None = None,
|
|
81
|
+
) -> dict[str, Any]:
|
|
82
|
+
"""
|
|
83
|
+
Invoke Bedrock LLM and return parsed JSON response.
|
|
84
|
+
|
|
85
|
+
The prompt should explicitly ask for JSON output.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
prompt: User prompt (should request JSON output)
|
|
89
|
+
model_id: Model ID (uses AWS_BEDROCK_MODEL_ID env var if not specified)
|
|
90
|
+
max_tokens: Maximum tokens to generate
|
|
91
|
+
temperature: Sampling temperature (0.0 to 1.0)
|
|
92
|
+
system_prompt: Optional system prompt
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Parsed JSON response as dictionary
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
BedrockError: If invocation fails or response is not valid JSON
|
|
99
|
+
"""
|
|
100
|
+
# Add JSON instruction if not present
|
|
101
|
+
json_prompt = prompt
|
|
102
|
+
if "json" not in prompt.lower():
|
|
103
|
+
json_prompt = f"{prompt}\n\nPlease respond with valid JSON only."
|
|
104
|
+
|
|
105
|
+
text_response = invoke(
|
|
106
|
+
prompt=json_prompt,
|
|
107
|
+
model_id=model_id,
|
|
108
|
+
max_tokens=max_tokens,
|
|
109
|
+
temperature=temperature,
|
|
110
|
+
system_prompt=system_prompt,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
# Try to parse the response as JSON
|
|
115
|
+
# Handle cases where model wraps JSON in markdown code blocks
|
|
116
|
+
cleaned = text_response.strip()
|
|
117
|
+
if cleaned.startswith("```json"):
|
|
118
|
+
cleaned = cleaned[7:]
|
|
119
|
+
if cleaned.startswith("```"):
|
|
120
|
+
cleaned = cleaned[3:]
|
|
121
|
+
if cleaned.endswith("```"):
|
|
122
|
+
cleaned = cleaned[:-3]
|
|
123
|
+
cleaned = cleaned.strip()
|
|
124
|
+
|
|
125
|
+
return json.loads(cleaned)
|
|
126
|
+
except json.JSONDecodeError as e:
|
|
127
|
+
raise BedrockError(
|
|
128
|
+
f"Model response is not valid JSON. Response: {text_response[:200]}..."
|
|
129
|
+
) from e
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _build_anthropic_request(
|
|
133
|
+
prompt: str,
|
|
134
|
+
max_tokens: int,
|
|
135
|
+
temperature: float,
|
|
136
|
+
system_prompt: str | None = None,
|
|
137
|
+
) -> dict[str, Any]:
|
|
138
|
+
"""Build request body for Anthropic Claude models."""
|
|
139
|
+
body: dict[str, Any] = {
|
|
140
|
+
"anthropic_version": "bedrock-2023-05-31",
|
|
141
|
+
"max_tokens": max_tokens,
|
|
142
|
+
"temperature": temperature,
|
|
143
|
+
"messages": [
|
|
144
|
+
{
|
|
145
|
+
"role": "user",
|
|
146
|
+
"content": prompt,
|
|
147
|
+
}
|
|
148
|
+
],
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if system_prompt:
|
|
152
|
+
body["system"] = system_prompt
|
|
153
|
+
|
|
154
|
+
return body
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _extract_anthropic_text(response_body: dict[str, Any]) -> str:
|
|
158
|
+
"""Extract text from Anthropic Claude response."""
|
|
159
|
+
content = response_body.get("content", [])
|
|
160
|
+
if not content:
|
|
161
|
+
raise BedrockError("Empty response from model")
|
|
162
|
+
|
|
163
|
+
# Claude returns content as list of content blocks
|
|
164
|
+
text_parts = []
|
|
165
|
+
for block in content:
|
|
166
|
+
if block.get("type") == "text":
|
|
167
|
+
text_parts.append(block.get("text", ""))
|
|
168
|
+
|
|
169
|
+
return "".join(text_parts)
|
aws_simple/config.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Configuration management via environment variables."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from dotenv import load_dotenv
|
|
6
|
+
|
|
7
|
+
from .exceptions import ConfigurationError
|
|
8
|
+
|
|
9
|
+
# Load .env file if present
|
|
10
|
+
load_dotenv()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Config:
|
|
14
|
+
"""Centralized configuration for AWS services."""
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def _get_required(key: str) -> str:
|
|
18
|
+
"""Get required environment variable or raise error."""
|
|
19
|
+
value = os.getenv(key)
|
|
20
|
+
if not value:
|
|
21
|
+
raise ConfigurationError(
|
|
22
|
+
f"Missing required environment variable: {key}. "
|
|
23
|
+
f"Please set it in your environment or .env file."
|
|
24
|
+
)
|
|
25
|
+
return value
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def _get_optional(key: str, default: str | None = None) -> str | None:
|
|
29
|
+
"""Get optional environment variable with default."""
|
|
30
|
+
return os.getenv(key, default)
|
|
31
|
+
|
|
32
|
+
# AWS General
|
|
33
|
+
@property
|
|
34
|
+
def aws_region(self) -> str:
|
|
35
|
+
"""AWS region (default: us-east-1)."""
|
|
36
|
+
return self._get_optional("AWS_REGION", "us-east-1") or "us-east-1"
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def aws_profile(self) -> str | None:
|
|
40
|
+
"""AWS profile name (optional, for local development)."""
|
|
41
|
+
return self._get_optional("AWS_PROFILE")
|
|
42
|
+
|
|
43
|
+
# S3
|
|
44
|
+
@property
|
|
45
|
+
def s3_bucket(self) -> str:
|
|
46
|
+
"""Default S3 bucket name."""
|
|
47
|
+
return self._get_required("AWS_S3_BUCKET")
|
|
48
|
+
|
|
49
|
+
# Textract
|
|
50
|
+
@property
|
|
51
|
+
def textract_region(self) -> str:
|
|
52
|
+
"""Textract region (defaults to aws_region)."""
|
|
53
|
+
return self._get_optional("AWS_TEXTRACT_REGION") or self.aws_region
|
|
54
|
+
|
|
55
|
+
# Bedrock
|
|
56
|
+
@property
|
|
57
|
+
def bedrock_model_id(self) -> str:
|
|
58
|
+
"""Default Bedrock model ID."""
|
|
59
|
+
return (
|
|
60
|
+
self._get_optional("AWS_BEDROCK_MODEL_ID", "anthropic.claude-3-5-sonnet-20241022-v2:0")
|
|
61
|
+
or "anthropic.claude-3-5-sonnet-20241022-v2:0"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def bedrock_region(self) -> str:
|
|
66
|
+
"""Bedrock region (defaults to aws_region)."""
|
|
67
|
+
return self._get_optional("AWS_BEDROCK_REGION") or self.aws_region
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# Singleton instance
|
|
71
|
+
config = Config()
|
aws_simple/exceptions.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Custom exceptions for aws-simple library."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AWSSimpleError(Exception):
|
|
5
|
+
"""Base exception for all aws-simple errors."""
|
|
6
|
+
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ConfigurationError(AWSSimpleError):
|
|
11
|
+
"""Raised when required environment variables are missing or invalid."""
|
|
12
|
+
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class S3Error(AWSSimpleError):
|
|
17
|
+
"""Raised when S3 operations fail."""
|
|
18
|
+
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TextractError(AWSSimpleError):
|
|
23
|
+
"""Raised when Textract operations fail."""
|
|
24
|
+
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class BedrockError(AWSSimpleError):
|
|
29
|
+
"""Raised when Bedrock operations fail."""
|
|
30
|
+
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ClientInitializationError(AWSSimpleError):
|
|
35
|
+
"""Raised when AWS client initialization fails."""
|
|
36
|
+
|
|
37
|
+
pass
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Data models for Textract results."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class TextractLine:
|
|
9
|
+
"""Represents a line of text extracted from a document."""
|
|
10
|
+
|
|
11
|
+
text: str
|
|
12
|
+
confidence: float
|
|
13
|
+
bounding_box: dict[str, float] # {top, left, width, height}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class TextractTable:
|
|
18
|
+
"""Represents a table extracted from a document."""
|
|
19
|
+
|
|
20
|
+
rows: int
|
|
21
|
+
columns: int
|
|
22
|
+
cells: list[list[str]] # Matrix: rows x columns
|
|
23
|
+
confidence: float
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class TextractPage:
|
|
28
|
+
"""Represents a page in the extracted document."""
|
|
29
|
+
|
|
30
|
+
page_number: int
|
|
31
|
+
width: float
|
|
32
|
+
height: float
|
|
33
|
+
lines: list[TextractLine]
|
|
34
|
+
tables: list[TextractTable]
|
|
35
|
+
raw_text: str # All text from this page concatenated
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class TextractDocument:
|
|
40
|
+
"""
|
|
41
|
+
Structured document result from Textract.
|
|
42
|
+
|
|
43
|
+
This is the main output format - clean, serializable JSON structure.
|
|
44
|
+
No AWS Blocks exposed.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
pages: list[TextractPage]
|
|
48
|
+
full_text: str # All text concatenated across all pages
|
|
49
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
50
|
+
|
|
51
|
+
def to_dict(self) -> dict[str, Any]:
|
|
52
|
+
"""Convert to dictionary for JSON serialization."""
|
|
53
|
+
return {
|
|
54
|
+
"pages": [
|
|
55
|
+
{
|
|
56
|
+
"page_number": page.page_number,
|
|
57
|
+
"width": page.width,
|
|
58
|
+
"height": page.height,
|
|
59
|
+
"lines": [
|
|
60
|
+
{
|
|
61
|
+
"text": line.text,
|
|
62
|
+
"confidence": line.confidence,
|
|
63
|
+
"bounding_box": line.bounding_box,
|
|
64
|
+
}
|
|
65
|
+
for line in page.lines
|
|
66
|
+
],
|
|
67
|
+
"tables": [
|
|
68
|
+
{
|
|
69
|
+
"rows": table.rows,
|
|
70
|
+
"columns": table.columns,
|
|
71
|
+
"cells": table.cells,
|
|
72
|
+
"confidence": table.confidence,
|
|
73
|
+
}
|
|
74
|
+
for table in page.tables
|
|
75
|
+
],
|
|
76
|
+
"raw_text": page.raw_text,
|
|
77
|
+
}
|
|
78
|
+
for page in self.pages
|
|
79
|
+
],
|
|
80
|
+
"full_text": self.full_text,
|
|
81
|
+
"metadata": self.metadata,
|
|
82
|
+
}
|