rakam-systems-core 0.1.1rc7__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.
- rakam_systems_core/__init__.py +41 -0
- rakam_systems_core/ai_core/__init__.py +68 -0
- rakam_systems_core/ai_core/base.py +142 -0
- rakam_systems_core/ai_core/config.py +12 -0
- rakam_systems_core/ai_core/config_loader.py +580 -0
- rakam_systems_core/ai_core/config_schema.py +395 -0
- rakam_systems_core/ai_core/interfaces/__init__.py +30 -0
- rakam_systems_core/ai_core/interfaces/agent.py +83 -0
- rakam_systems_core/ai_core/interfaces/chat_history.py +122 -0
- rakam_systems_core/ai_core/interfaces/chunker.py +11 -0
- rakam_systems_core/ai_core/interfaces/embedding_model.py +10 -0
- rakam_systems_core/ai_core/interfaces/indexer.py +10 -0
- rakam_systems_core/ai_core/interfaces/llm_gateway.py +139 -0
- rakam_systems_core/ai_core/interfaces/loader.py +86 -0
- rakam_systems_core/ai_core/interfaces/reranker.py +10 -0
- rakam_systems_core/ai_core/interfaces/retriever.py +11 -0
- rakam_systems_core/ai_core/interfaces/tool.py +162 -0
- rakam_systems_core/ai_core/interfaces/tool_invoker.py +260 -0
- rakam_systems_core/ai_core/interfaces/tool_loader.py +374 -0
- rakam_systems_core/ai_core/interfaces/tool_registry.py +287 -0
- rakam_systems_core/ai_core/interfaces/vectorstore.py +37 -0
- rakam_systems_core/ai_core/mcp/README.md +545 -0
- rakam_systems_core/ai_core/mcp/__init__.py +0 -0
- rakam_systems_core/ai_core/mcp/mcp_server.py +334 -0
- rakam_systems_core/ai_core/tracking.py +602 -0
- rakam_systems_core/ai_core/vs_core.py +55 -0
- rakam_systems_core/ai_utils/__init__.py +16 -0
- rakam_systems_core/ai_utils/logging.py +126 -0
- rakam_systems_core/ai_utils/metrics.py +10 -0
- rakam_systems_core/ai_utils/s3.py +480 -0
- rakam_systems_core/ai_utils/tracing.py +5 -0
- rakam_systems_core-0.1.1rc7.dist-info/METADATA +162 -0
- rakam_systems_core-0.1.1rc7.dist-info/RECORD +34 -0
- rakam_systems_core-0.1.1rc7.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Lightweight logging utilities for rakam_systems.
|
|
5
|
+
|
|
6
|
+
This module is a very thin wrapper around Python's standard :mod:`logging`
|
|
7
|
+
library that provides:
|
|
8
|
+
|
|
9
|
+
- A single place to configure default log format and level
|
|
10
|
+
- A stable import path for all internal modules:
|
|
11
|
+
|
|
12
|
+
>>> from rakam_systems_core.ai_utils import logging
|
|
13
|
+
>>> logger = logging.getLogger(__name__)
|
|
14
|
+
>>> logger.info("hello")
|
|
15
|
+
|
|
16
|
+
It intentionally mirrors the standard logging API so most existing code
|
|
17
|
+
can simply replace ``import logging`` with::
|
|
18
|
+
|
|
19
|
+
from rakam_systems_core.ai_utils import logging
|
|
20
|
+
|
|
21
|
+
and continue to work as before.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import logging as _logging
|
|
25
|
+
import os
|
|
26
|
+
from typing import Any, Optional
|
|
27
|
+
|
|
28
|
+
# Re-export common types and level constants so callers can use
|
|
29
|
+
# logging.INFO, logging.WARNING, etc.
|
|
30
|
+
Logger = _logging.Logger
|
|
31
|
+
|
|
32
|
+
DEBUG = _logging.DEBUG
|
|
33
|
+
INFO = _logging.INFO
|
|
34
|
+
WARNING = _logging.WARNING
|
|
35
|
+
ERROR = _logging.ERROR
|
|
36
|
+
CRITICAL = _logging.CRITICAL
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
_DEFAULT_LEVEL_NAME = os.getenv("RAKAM_LOG_LEVEL", "INFO").upper()
|
|
40
|
+
_DEFAULT_LEVEL = getattr(_logging, _DEFAULT_LEVEL_NAME, _logging.INFO)
|
|
41
|
+
_DEFAULT_FORMAT = os.getenv(
|
|
42
|
+
"RAKAM_LOG_FORMAT",
|
|
43
|
+
"%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
44
|
+
)
|
|
45
|
+
_DEFAULT_DATEFMT = os.getenv("RAKAM_LOG_DATEFMT", "%Y-%m-%d %H:%M:%S")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def basicConfig(
|
|
49
|
+
*,
|
|
50
|
+
level: int | str = _DEFAULT_LEVEL,
|
|
51
|
+
format: str = _DEFAULT_FORMAT,
|
|
52
|
+
datefmt: Optional[str] = _DEFAULT_DATEFMT,
|
|
53
|
+
**kwargs: Any,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""
|
|
56
|
+
Configure the root logger.
|
|
57
|
+
|
|
58
|
+
This is a thin wrapper over :func:`logging.basicConfig` that also
|
|
59
|
+
accepts string log levels (\"INFO\", \"DEBUG\", ...).
|
|
60
|
+
"""
|
|
61
|
+
if isinstance(level, str):
|
|
62
|
+
level = getattr(_logging, level.upper(), _DEFAULT_LEVEL)
|
|
63
|
+
|
|
64
|
+
_logging.basicConfig(level=level, format=format, datefmt=datefmt, **kwargs)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _ensure_basic_config() -> None:
|
|
68
|
+
"""
|
|
69
|
+
Ensure there is at least a minimal logging configuration.
|
|
70
|
+
|
|
71
|
+
If user code has already configured logging (root logger has handlers),
|
|
72
|
+
this function is a no-op.
|
|
73
|
+
"""
|
|
74
|
+
root = _logging.getLogger()
|
|
75
|
+
if root.handlers:
|
|
76
|
+
return
|
|
77
|
+
basicConfig()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def get_logger(name: str = "ai") -> Logger:
|
|
81
|
+
"""
|
|
82
|
+
Return a configured logger instance.
|
|
83
|
+
|
|
84
|
+
This is the preferred entry point for application code. It mirrors the
|
|
85
|
+
standard :func:`logging.getLogger` but ensures a basic configuration
|
|
86
|
+
exists first.
|
|
87
|
+
"""
|
|
88
|
+
_ensure_basic_config()
|
|
89
|
+
return _logging.getLogger(name)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def getLogger(name: str = "ai") -> Logger:
|
|
93
|
+
"""
|
|
94
|
+
Alias for :func:`get_logger` to match the stdlib API.
|
|
95
|
+
|
|
96
|
+
Allows existing code that uses ``logging.getLogger`` to keep working
|
|
97
|
+
when importing this module as ``logging``.
|
|
98
|
+
"""
|
|
99
|
+
return get_logger(name)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def setLevel(level: int | str) -> None:
|
|
103
|
+
"""
|
|
104
|
+
Convenience helper to set the global root log level.
|
|
105
|
+
|
|
106
|
+
Example:
|
|
107
|
+
>>> from rakam_systems_core.ai_utils import logging
|
|
108
|
+
>>> logging.setLevel("DEBUG")
|
|
109
|
+
"""
|
|
110
|
+
if isinstance(level, str):
|
|
111
|
+
level = getattr(_logging, level.upper(), _DEFAULT_LEVEL)
|
|
112
|
+
_logging.getLogger().setLevel(level)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
__all__ = [
|
|
116
|
+
"Logger",
|
|
117
|
+
"DEBUG",
|
|
118
|
+
"INFO",
|
|
119
|
+
"WARNING",
|
|
120
|
+
"ERROR",
|
|
121
|
+
"CRITICAL",
|
|
122
|
+
"basicConfig",
|
|
123
|
+
"get_logger",
|
|
124
|
+
"getLogger",
|
|
125
|
+
"setLevel",
|
|
126
|
+
]
|
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Lightweight S3 utilities for rakam_systems.
|
|
5
|
+
|
|
6
|
+
This module provides a thin wrapper around boto3's S3 client with:
|
|
7
|
+
|
|
8
|
+
- Environment-based configuration (credentials, endpoint, region, bucket)
|
|
9
|
+
- Simple CRUD operations (upload, download, delete, list)
|
|
10
|
+
- Support for S3-compatible services (OVH, Scaleway, MinIO, etc.)
|
|
11
|
+
- Consistent error handling and logging
|
|
12
|
+
- A stable import path for all internal modules:
|
|
13
|
+
|
|
14
|
+
>>> from rakam_systems_core.ai_utils import s3
|
|
15
|
+
>>> s3.upload_file("my-key.txt", "Hello World")
|
|
16
|
+
>>> content = s3.download_file("my-key.txt") # Returns bytes
|
|
17
|
+
>>> text = content.decode('utf-8') # Decode to string if needed
|
|
18
|
+
|
|
19
|
+
Configuration is read from environment variables:
|
|
20
|
+
- S3_ACCESS_KEY: AWS/S3 access key ID (required)
|
|
21
|
+
- S3_SECRET_KEY: AWS/S3 secret access key (required)
|
|
22
|
+
- S3_BUCKET_NAME: Default bucket name (required)
|
|
23
|
+
- S3_ENDPOINT_URL: Custom endpoint for S3-compatible services (optional)
|
|
24
|
+
- S3_REGION: AWS region or provider region code (default: "gra")
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import os
|
|
28
|
+
from typing import Any, Dict, List, Optional
|
|
29
|
+
from datetime import datetime
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
import boto3
|
|
33
|
+
from botocore.exceptions import ClientError
|
|
34
|
+
except ImportError:
|
|
35
|
+
raise ImportError(
|
|
36
|
+
"boto3 is required for S3 utilities. Install with: pip install boto3"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# Configuration from environment variables
|
|
41
|
+
_S3_ACCESS_KEY = os.getenv("S3_ACCESS_KEY")
|
|
42
|
+
_S3_SECRET_KEY = os.getenv("S3_SECRET_KEY")
|
|
43
|
+
_S3_ENDPOINT_URL = os.getenv(
|
|
44
|
+
"S3_ENDPOINT_URL", "https://s3.gra.io.cloud.ovh.net")
|
|
45
|
+
_S3_REGION = os.getenv("S3_REGION", "gra")
|
|
46
|
+
_S3_BUCKET_NAME = os.getenv("S3_BUCKET_NAME")
|
|
47
|
+
|
|
48
|
+
# Singleton client instance
|
|
49
|
+
_client: Optional[Any] = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class S3Error(Exception):
|
|
53
|
+
"""Base exception for S3 operations."""
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class S3ConfigError(S3Error):
|
|
58
|
+
"""Raised when S3 configuration is invalid or missing."""
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class S3NotFoundError(S3Error):
|
|
63
|
+
"""Raised when a requested S3 object is not found."""
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class S3PermissionError(S3Error):
|
|
68
|
+
"""Raised when access to S3 resource is denied."""
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _validate_config() -> None:
|
|
73
|
+
"""
|
|
74
|
+
Validate that all required S3 configuration is present.
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
S3ConfigError: If required configuration is missing.
|
|
78
|
+
"""
|
|
79
|
+
missing = []
|
|
80
|
+
if not _S3_ACCESS_KEY:
|
|
81
|
+
missing.append("S3_ACCESS_KEY")
|
|
82
|
+
if not _S3_SECRET_KEY:
|
|
83
|
+
missing.append("S3_SECRET_KEY")
|
|
84
|
+
if not _S3_BUCKET_NAME:
|
|
85
|
+
missing.append("S3_BUCKET_NAME")
|
|
86
|
+
|
|
87
|
+
if missing:
|
|
88
|
+
raise S3ConfigError(
|
|
89
|
+
f"Missing required environment variables: {', '.join(missing)}. "
|
|
90
|
+
f"Please set these in your environment or .env file."
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def get_client() -> Any:
|
|
95
|
+
"""
|
|
96
|
+
Get or create the S3 client singleton.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
boto3.client: Configured S3 client instance.
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
S3ConfigError: If required configuration is missing.
|
|
103
|
+
"""
|
|
104
|
+
global _client
|
|
105
|
+
|
|
106
|
+
if _client is not None:
|
|
107
|
+
return _client
|
|
108
|
+
|
|
109
|
+
_validate_config()
|
|
110
|
+
|
|
111
|
+
client_config = {
|
|
112
|
+
'aws_access_key_id': _S3_ACCESS_KEY,
|
|
113
|
+
'aws_secret_access_key': _S3_SECRET_KEY,
|
|
114
|
+
'region_name': _S3_REGION
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# Add endpoint URL if specified (for S3-compatible services)
|
|
118
|
+
if _S3_ENDPOINT_URL:
|
|
119
|
+
client_config['endpoint_url'] = _S3_ENDPOINT_URL
|
|
120
|
+
|
|
121
|
+
_client = boto3.client('s3', **client_config)
|
|
122
|
+
return _client
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def reset_client() -> None:
|
|
126
|
+
"""
|
|
127
|
+
Reset the S3 client singleton.
|
|
128
|
+
|
|
129
|
+
Useful for testing or when credentials need to be refreshed.
|
|
130
|
+
"""
|
|
131
|
+
global _client
|
|
132
|
+
_client = None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def upload_file(
|
|
136
|
+
key: str,
|
|
137
|
+
content: str | bytes,
|
|
138
|
+
bucket: Optional[str] = None,
|
|
139
|
+
content_type: str = "text/plain",
|
|
140
|
+
metadata: Optional[Dict[str, str]] = None
|
|
141
|
+
) -> bool:
|
|
142
|
+
"""
|
|
143
|
+
Upload a file to S3.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
key: The S3 object key (path/filename).
|
|
147
|
+
content: File content as string or bytes.
|
|
148
|
+
bucket: Bucket name (defaults to S3_BUCKET_NAME).
|
|
149
|
+
content_type: MIME type of the content.
|
|
150
|
+
metadata: Optional metadata dictionary.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
bool: True if upload succeeded.
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
S3Error: If upload fails.
|
|
157
|
+
"""
|
|
158
|
+
client = get_client()
|
|
159
|
+
bucket = bucket or _S3_BUCKET_NAME
|
|
160
|
+
|
|
161
|
+
# Convert string to bytes if needed
|
|
162
|
+
if isinstance(content, str):
|
|
163
|
+
content = content.encode('utf-8')
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
put_args = {
|
|
167
|
+
'Bucket': bucket,
|
|
168
|
+
'Key': key,
|
|
169
|
+
'Body': content,
|
|
170
|
+
'ContentType': content_type
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if metadata:
|
|
174
|
+
put_args['Metadata'] = metadata
|
|
175
|
+
|
|
176
|
+
client.put_object(**put_args)
|
|
177
|
+
return True
|
|
178
|
+
|
|
179
|
+
except ClientError as e:
|
|
180
|
+
error_code = e.response.get('Error', {}).get('Code', 'Unknown')
|
|
181
|
+
raise S3Error(f"Failed to upload file '{key}': {error_code} - {e}")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def download_file(
|
|
185
|
+
key: str,
|
|
186
|
+
bucket: Optional[str] = None
|
|
187
|
+
) -> bytes:
|
|
188
|
+
"""
|
|
189
|
+
Download a file from S3.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
key: The S3 object key (path/filename).
|
|
193
|
+
bucket: Bucket name (defaults to S3_BUCKET_NAME).
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
bytes: File content as bytes. The application should decode if needed
|
|
197
|
+
(e.g., content.decode('utf-8') for text files).
|
|
198
|
+
|
|
199
|
+
Raises:
|
|
200
|
+
S3NotFoundError: If file does not exist.
|
|
201
|
+
S3Error: If download fails.
|
|
202
|
+
|
|
203
|
+
Note:
|
|
204
|
+
This function always returns bytes to preserve data integrity and support
|
|
205
|
+
all file types (text, binary, images, PDFs, etc.). For text files, decode
|
|
206
|
+
the result: content.decode('utf-8')
|
|
207
|
+
"""
|
|
208
|
+
client = get_client()
|
|
209
|
+
bucket = bucket or _S3_BUCKET_NAME
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
response = client.get_object(Bucket=bucket, Key=key)
|
|
213
|
+
content = response['Body'].read()
|
|
214
|
+
return content
|
|
215
|
+
|
|
216
|
+
except ClientError as e:
|
|
217
|
+
error_code = e.response.get('Error', {}).get('Code', 'Unknown')
|
|
218
|
+
if error_code == 'NoSuchKey' or error_code == '404':
|
|
219
|
+
raise S3NotFoundError(f"File not found: '{key}'")
|
|
220
|
+
raise S3Error(f"Failed to download file '{key}': {error_code} - {e}")
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def delete_file(key: str, bucket: Optional[str] = None) -> bool:
|
|
224
|
+
"""
|
|
225
|
+
Delete a file from S3.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
key: The S3 object key (path/filename).
|
|
229
|
+
bucket: Bucket name (defaults to S3_BUCKET_NAME).
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
bool: True if deletion succeeded.
|
|
233
|
+
|
|
234
|
+
Raises:
|
|
235
|
+
S3Error: If deletion fails.
|
|
236
|
+
"""
|
|
237
|
+
client = get_client()
|
|
238
|
+
bucket = bucket or _S3_BUCKET_NAME
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
client.delete_object(Bucket=bucket, Key=key)
|
|
242
|
+
return True
|
|
243
|
+
|
|
244
|
+
except ClientError as e:
|
|
245
|
+
error_code = e.response.get('Error', {}).get('Code', 'Unknown')
|
|
246
|
+
raise S3Error(f"Failed to delete file '{key}': {error_code} - {e}")
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def file_exists(key: str, bucket: Optional[str] = None) -> bool:
|
|
250
|
+
"""
|
|
251
|
+
Check if a file exists in S3.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
key: The S3 object key (path/filename).
|
|
255
|
+
bucket: Bucket name (defaults to S3_BUCKET_NAME).
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
bool: True if file exists, False otherwise.
|
|
259
|
+
"""
|
|
260
|
+
client = get_client()
|
|
261
|
+
bucket = bucket or _S3_BUCKET_NAME
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
client.head_object(Bucket=bucket, Key=key)
|
|
265
|
+
return True
|
|
266
|
+
except ClientError as e:
|
|
267
|
+
error_code = e.response.get('Error', {}).get('Code', 'Unknown')
|
|
268
|
+
if error_code == 'NoSuchKey' or error_code == '404':
|
|
269
|
+
return False
|
|
270
|
+
# Re-raise other errors
|
|
271
|
+
raise S3Error(
|
|
272
|
+
f"Failed to check file existence '{key}': {error_code} - {e}")
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def list_files(
|
|
276
|
+
prefix: str = "",
|
|
277
|
+
bucket: Optional[str] = None,
|
|
278
|
+
max_keys: int = 1000
|
|
279
|
+
) -> List[Dict[str, Any]]:
|
|
280
|
+
"""
|
|
281
|
+
List files in S3 bucket with optional prefix filter.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
prefix: Filter results to keys starting with this prefix.
|
|
285
|
+
bucket: Bucket name (defaults to S3_BUCKET_NAME).
|
|
286
|
+
max_keys: Maximum number of keys to return.
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
List[Dict]: List of file metadata dictionaries with keys:
|
|
290
|
+
- Key: Object key (str)
|
|
291
|
+
- Size: File size in bytes (int)
|
|
292
|
+
- LastModified: Last modification datetime
|
|
293
|
+
- ETag: Entity tag (str)
|
|
294
|
+
"""
|
|
295
|
+
client = get_client()
|
|
296
|
+
bucket = bucket or _S3_BUCKET_NAME
|
|
297
|
+
|
|
298
|
+
try:
|
|
299
|
+
response = client.list_objects_v2(
|
|
300
|
+
Bucket=bucket,
|
|
301
|
+
Prefix=prefix,
|
|
302
|
+
MaxKeys=max_keys
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
if 'Contents' not in response:
|
|
306
|
+
return []
|
|
307
|
+
|
|
308
|
+
return [
|
|
309
|
+
{
|
|
310
|
+
'Key': obj['Key'],
|
|
311
|
+
'Size': obj['Size'],
|
|
312
|
+
'LastModified': obj['LastModified'],
|
|
313
|
+
'ETag': obj.get('ETag', '')
|
|
314
|
+
}
|
|
315
|
+
for obj in response['Contents']
|
|
316
|
+
]
|
|
317
|
+
|
|
318
|
+
except ClientError as e:
|
|
319
|
+
error_code = e.response.get('Error', {}).get('Code', 'Unknown')
|
|
320
|
+
raise S3Error(
|
|
321
|
+
f"Failed to list files with prefix '{prefix}': {error_code} - {e}")
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def get_file_metadata(key: str, bucket: Optional[str] = None) -> Dict[str, Any]:
|
|
325
|
+
"""
|
|
326
|
+
Get metadata for a file without downloading it.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
key: The S3 object key (path/filename).
|
|
330
|
+
bucket: Bucket name (defaults to S3_BUCKET_NAME).
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
Dict: Metadata dictionary with keys like ContentType, ContentLength,
|
|
334
|
+
LastModified, ETag, Metadata (custom metadata).
|
|
335
|
+
|
|
336
|
+
Raises:
|
|
337
|
+
S3NotFoundError: If file does not exist.
|
|
338
|
+
S3Error: If operation fails.
|
|
339
|
+
"""
|
|
340
|
+
client = get_client()
|
|
341
|
+
bucket = bucket or _S3_BUCKET_NAME
|
|
342
|
+
|
|
343
|
+
try:
|
|
344
|
+
response = client.head_object(Bucket=bucket, Key=key)
|
|
345
|
+
return {
|
|
346
|
+
'ContentType': response.get('ContentType', ''),
|
|
347
|
+
'ContentLength': response.get('ContentLength', 0),
|
|
348
|
+
'LastModified': response.get('LastModified'),
|
|
349
|
+
'ETag': response.get('ETag', ''),
|
|
350
|
+
'Metadata': response.get('Metadata', {})
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
except ClientError as e:
|
|
354
|
+
error_code = e.response.get('Error', {}).get('Code', 'Unknown')
|
|
355
|
+
if error_code == 'NoSuchKey' or error_code == '404':
|
|
356
|
+
raise S3NotFoundError(f"File not found: '{key}'")
|
|
357
|
+
raise S3Error(
|
|
358
|
+
f"Failed to get metadata for '{key}': {error_code} - {e}")
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def create_bucket(bucket: Optional[str] = None) -> bool:
|
|
362
|
+
"""
|
|
363
|
+
Create an S3 bucket.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
bucket: Bucket name (defaults to S3_BUCKET_NAME).
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
bool: True if bucket was created or already exists.
|
|
370
|
+
|
|
371
|
+
Raises:
|
|
372
|
+
S3Error: If bucket creation fails.
|
|
373
|
+
"""
|
|
374
|
+
client = get_client()
|
|
375
|
+
bucket = bucket or _S3_BUCKET_NAME
|
|
376
|
+
|
|
377
|
+
try:
|
|
378
|
+
client.create_bucket(Bucket=bucket)
|
|
379
|
+
return True
|
|
380
|
+
|
|
381
|
+
except ClientError as e:
|
|
382
|
+
error_code = e.response.get('Error', {}).get('Code', 'Unknown')
|
|
383
|
+
|
|
384
|
+
# Bucket already exists cases
|
|
385
|
+
if error_code in ('BucketAlreadyOwnedByYou', 'BucketAlreadyExists'):
|
|
386
|
+
return True
|
|
387
|
+
|
|
388
|
+
raise S3Error(
|
|
389
|
+
f"Failed to create bucket '{bucket}': {error_code} - {e}")
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def bucket_exists(bucket: Optional[str] = None) -> bool:
|
|
393
|
+
"""
|
|
394
|
+
Check if a bucket exists and is accessible.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
bucket: Bucket name (defaults to S3_BUCKET_NAME).
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
bool: True if bucket exists and is accessible.
|
|
401
|
+
"""
|
|
402
|
+
client = get_client()
|
|
403
|
+
bucket = bucket or _S3_BUCKET_NAME
|
|
404
|
+
|
|
405
|
+
try:
|
|
406
|
+
client.head_bucket(Bucket=bucket)
|
|
407
|
+
return True
|
|
408
|
+
except ClientError as e:
|
|
409
|
+
error_code = e.response.get('Error', {}).get('Code', 'Unknown')
|
|
410
|
+
if error_code in ('404', 'NoSuchBucket'):
|
|
411
|
+
return False
|
|
412
|
+
if error_code == '403':
|
|
413
|
+
raise S3PermissionError(f"Access denied to bucket '{bucket}'")
|
|
414
|
+
raise S3Error(f"Failed to check bucket '{bucket}': {error_code} - {e}")
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def list_buckets() -> List[Dict[str, Any]]:
|
|
418
|
+
"""
|
|
419
|
+
List all buckets in the account.
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
List[Dict]: List of bucket metadata dictionaries with keys:
|
|
423
|
+
- Name: Bucket name (str)
|
|
424
|
+
- CreationDate: Creation datetime
|
|
425
|
+
"""
|
|
426
|
+
client = get_client()
|
|
427
|
+
|
|
428
|
+
try:
|
|
429
|
+
response = client.list_buckets()
|
|
430
|
+
return [
|
|
431
|
+
{
|
|
432
|
+
'Name': bucket['Name'],
|
|
433
|
+
'CreationDate': bucket['CreationDate']
|
|
434
|
+
}
|
|
435
|
+
for bucket in response.get('Buckets', [])
|
|
436
|
+
]
|
|
437
|
+
|
|
438
|
+
except ClientError as e:
|
|
439
|
+
error_code = e.response.get('Error', {}).get('Code', 'Unknown')
|
|
440
|
+
raise S3Error(f"Failed to list buckets: {error_code} - {e}")
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def get_config() -> Dict[str, Any]:
|
|
444
|
+
"""
|
|
445
|
+
Get current S3 configuration.
|
|
446
|
+
|
|
447
|
+
Returns:
|
|
448
|
+
Dict: Configuration dictionary (secrets are masked).
|
|
449
|
+
"""
|
|
450
|
+
return {
|
|
451
|
+
'bucket': _S3_BUCKET_NAME,
|
|
452
|
+
'region': _S3_REGION,
|
|
453
|
+
'endpoint_url': _S3_ENDPOINT_URL,
|
|
454
|
+
'access_key': _S3_ACCESS_KEY[:4] + '***' if _S3_ACCESS_KEY else None,
|
|
455
|
+
'has_secret_key': bool(_S3_SECRET_KEY)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
__all__ = [
|
|
460
|
+
# Exceptions
|
|
461
|
+
"S3Error",
|
|
462
|
+
"S3ConfigError",
|
|
463
|
+
"S3NotFoundError",
|
|
464
|
+
"S3PermissionError",
|
|
465
|
+
# Client management
|
|
466
|
+
"get_client",
|
|
467
|
+
"reset_client",
|
|
468
|
+
"get_config",
|
|
469
|
+
# File operations
|
|
470
|
+
"upload_file",
|
|
471
|
+
"download_file",
|
|
472
|
+
"delete_file",
|
|
473
|
+
"file_exists",
|
|
474
|
+
"list_files",
|
|
475
|
+
"get_file_metadata",
|
|
476
|
+
# Bucket operations
|
|
477
|
+
"create_bucket",
|
|
478
|
+
"bucket_exists",
|
|
479
|
+
"list_buckets",
|
|
480
|
+
]
|