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 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,5 @@
1
+ """Internal parsers for AWS service responses."""
2
+
3
+ from .textract_parser import TextractParser
4
+
5
+ __all__ = ["TextractParser"]
@@ -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()
@@ -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,10 @@
1
+ """Data models for aws-simple."""
2
+
3
+ from .textract import TextractDocument, TextractLine, TextractPage, TextractTable
4
+
5
+ __all__ = [
6
+ "TextractDocument",
7
+ "TextractLine",
8
+ "TextractPage",
9
+ "TextractTable",
10
+ ]
@@ -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
+ }