Skip to content

Commit 6659b55

Browse files
committed
[feat] #15 Add S3 storage strategy - better handle exceptions and move resolve placeholder to storage strategy
1 parent 6977245 commit 6659b55

File tree

2 files changed

+59
-30
lines changed

2 files changed

+59
-30
lines changed

app/storage_strategies/aws_s3.py

Lines changed: 45 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,37 @@
11
import boto3
2-
import os
3-
from string import Template
4-
from botocore.exceptions import NoCredentialsError, ClientError
2+
from botocore.exceptions import EndpointConnectionError, ClientError
53
from storage_strategies.storage_strategy import StorageStrategy
64

75
class AWSS3StorageStrategy(StorageStrategy):
86
def __init__(self, context):
97
super().__init__(context)
108

11-
self.bucket_name = self._resolve_placeholder(context['settings'].get('bucket_name'))
12-
self.region = self._resolve_placeholder(context['settings'].get('region'))
13-
self.access_key = self._resolve_placeholder(context['settings'].get('access_key'))
14-
self.secret_access_key = self._resolve_placeholder(context['settings'].get('secret_access_key'))
9+
self.bucket_name = self.resolve_placeholder(context['settings'].get('bucket_name'))
10+
self.region = self.resolve_placeholder(context['settings'].get('region'))
11+
self.access_key = self.resolve_placeholder(context['settings'].get('access_key'))
12+
self.secret_access_key = self.resolve_placeholder(context['settings'].get('secret_access_key'))
1513

16-
self.s3_client = boto3.client(
17-
's3',
18-
aws_access_key_id=self.access_key,
19-
aws_secret_access_key=self.secret_access_key,
20-
region_name=self.region
21-
)
22-
23-
def _resolve_placeholder(self, value):
24-
if not value:
25-
return None
26-
return Template(value).substitute(os.environ)
14+
try:
15+
self.s3_client = boto3.client(
16+
's3',
17+
aws_access_key_id=self.access_key,
18+
aws_secret_access_key=self.secret_access_key,
19+
region_name=self.region
20+
)
21+
self.s3_client.head_bucket(Bucket=self.bucket_name)
22+
except EndpointConnectionError as e:
23+
raise RuntimeError(
24+
f"{str(e)}\n"
25+
"Check your AWS_REGION and AWS_S3_BUCKET_NAME environment variables."
26+
) from e
27+
except ClientError as e:
28+
error_code = e.response.get('Error', {}).get('Code', 'Unknown')
29+
if error_code in ('400', '403'):
30+
raise RuntimeError(
31+
f"{str(e)}\n"
32+
"Error: Please check your AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY."
33+
) from e
34+
raise
2735

2836
def save(self, file_name, dest_file_name, content):
2937
formatted_file_name = self.format_file_name(file_name, dest_file_name)
@@ -34,31 +42,40 @@ def save(self, file_name, dest_file_name, content):
3442
Key=formatted_file_name,
3543
Body=content.encode('utf-8')
3644
)
37-
print(f"File {formatted_file_name} saved to S3 bucket {self.bucket_name}.")
38-
except NoCredentialsError:
39-
print("AWS credentials are missing or invalid.")
4045
except ClientError as e:
41-
print(f"Failed to upload {file_name}: {e}")
46+
raise RuntimeError(
47+
f"{str(e)}\n"
48+
f"Error saving file '{file_name}' as '{formatted_file_name}' to bucket '{self.bucket_name}'."
49+
) from e
4250

4351
def load(self, file_name):
4452
try:
4553
response = self.s3_client.get_object(Bucket=self.bucket_name, Key=file_name)
4654
return response['Body'].read().decode('utf-8')
4755
except ClientError as e:
48-
print(f"Failed to download {file_name}: {e}")
49-
return None
56+
error_code = e.response['Error']['Code']
57+
if error_code == 'NoSuchKey':
58+
return None
59+
raise RuntimeError(
60+
f"{str(e)}\n"
61+
f"Error loading file '{file_name}' from bucket '{self.bucket_name}'."
62+
) from e
5063

5164
def list(self):
5265
try:
5366
response = self.s3_client.list_objects_v2(Bucket=self.bucket_name)
5467
return [item['Key'] for item in response.get('Contents', [])]
5568
except ClientError as e:
56-
print(f"Failed to list files: {e}")
57-
return []
69+
raise RuntimeError(
70+
f"{str(e)}\n"
71+
f"Error listing objects in bucket '{self.bucket_name}'."
72+
) from e
5873

5974
def delete(self, file_name):
6075
try:
6176
self.s3_client.delete_object(Bucket=self.bucket_name, Key=file_name)
62-
print(f"File {file_name} deleted from S3 bucket {self.bucket_name}.")
6377
except ClientError as e:
64-
print(f"Failed to delete {file_name}: {e}")
78+
raise RuntimeError(
79+
f"{str(e)}\n"
80+
f"Error deleting file '{file_name}' from bucket '{self.bucket_name}'."
81+
) from e

app/storage_strategies/storage_strategy.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import yaml
33
from datetime import datetime
44
from pathlib import Path
5+
from string import Template
56

67
class StorageStrategy:
78
def __init__(self, context):
@@ -18,7 +19,7 @@ def list(self):
1819

1920
def delete(self, file_name):
2021
raise NotImplementedError("Subclasses must implement this method")
21-
22+
2223
def format_file_name(self, file_name, format_string):
2324
return format_string.format(file_fullname=file_name, # file_name with path
2425
file_name=Path(file_name).stem, # file_name without path
@@ -28,4 +29,15 @@ def format_file_name(self, file_name, format_string):
2829
dd=datetime.now().strftime('%d'),
2930
HH=datetime.now().strftime('%H'),
3031
MM=datetime.now().strftime('%M'),
31-
SS=datetime.now().strftime('%S'))
32+
SS=datetime.now().strftime('%S'))
33+
34+
def resolve_placeholder(self, value, default=None):
35+
if not value:
36+
return default
37+
try:
38+
return Template(value).substitute(os.environ)
39+
except KeyError as e:
40+
if default:
41+
return default
42+
else:
43+
raise ValueError(f"Environment variable '{e.args[0]}' is missing, and no default value is provided.")

0 commit comments

Comments
 (0)