boto3-assist 0.2.0__tar.gz → 0.2.2__tar.gz
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.
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/PKG-INFO +1 -1
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/pyproject.toml +1 -1
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/requirements-dev.txt +1 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/src/boto3_assist/cloudwatch/cloudwatch_connection.py +2 -4
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/src/boto3_assist/dynamodb/dynamodb.py +2 -2
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/src/boto3_assist/dynamodb/dynamodb_connection.py +28 -2
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/src/boto3_assist/dynamodb/dynamodb_index.py +6 -2
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/src/boto3_assist/dynamodb/dynamodb_model_base.py +2 -3
- boto3_assist-0.2.2/src/boto3_assist/errors/custom_exceptions.py +34 -0
- boto3_assist-0.2.2/src/boto3_assist/s3/s3.py +505 -0
- boto3_assist-0.2.2/src/boto3_assist/s3/s3_connection.py +120 -0
- boto3_assist-0.2.2/src/boto3_assist/utilities/file_operations.py +105 -0
- boto3_assist-0.2.2/src/boto3_assist/utilities/http_utility.py +42 -0
- boto3_assist-0.2.2/src/boto3_assist/version.py +1 -0
- boto3_assist-0.2.2/tests/dynamodb/models/user_model.py +64 -0
- boto3_assist-0.2.0/src/boto3_assist/version.py +0 -1
- boto3_assist-0.2.0/tests/dynamodb/models/user_model.py +0 -79
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/.env.docker +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/.env.docker.001 +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/.env.docker.nosql.workbench +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/.env.unittest +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/.gitignore +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/.vscode/launch.json +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/.vscode/settings.json +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/.vscode/tasks.json +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/LICENSE-EXPLAINED.txt +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/LICENSE.txt +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/README.md +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/aws_regions_with_status.csv +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/aws_regions_with_status.json +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/devops/build.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/devops/readme.md +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/examples/__init__.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/examples/cloudwatch/log_report.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/examples/dynamodb/models/order_item_model.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/examples/dynamodb/models/order_model.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/examples/dynamodb/models/product_model.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/examples/dynamodb/models/user_model.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/examples/dynamodb/models/user_post_model.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/examples/dynamodb/order_example/main.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/examples/dynamodb/order_example/products.json +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/examples/dynamodb/services/order_item_service.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/examples/dynamodb/services/order_service.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/examples/dynamodb/services/product_service.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/examples/dynamodb/services/table_service.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/examples/dynamodb/services/user_post_service.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/examples/dynamodb/services/user_service.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/examples/dynamodb/services/user_service_client_example.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/examples/dynamodb/services/user_service_resource_example.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/examples/dynamodb/user_post_example/main.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/examples/ec2/regions_report.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/module-headers.txt +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/mypy.ini +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/requirements.txt +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/run-checks.sh +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/src/boto3_assist/__init__.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/src/boto3_assist/boto3session.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/src/boto3_assist/cloudwatch/cloudwatch_connection_tracker.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/src/boto3_assist/cloudwatch/cloudwatch_log_connection.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/src/boto3_assist/cloudwatch/cloudwatch_logs.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/src/boto3_assist/cloudwatch/cloudwatch_query.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/src/boto3_assist/connection.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/src/boto3_assist/connection_tracker.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/src/boto3_assist/dynamodb/dynamodb_connection_tracker.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/src/boto3_assist/dynamodb/dynamodb_helpers.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/src/boto3_assist/dynamodb/dynamodb_importer.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/src/boto3_assist/dynamodb/dynamodb_iservice.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/src/boto3_assist/dynamodb/dynamodb_key.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/src/boto3_assist/dynamodb/dynamodb_model_base_interfaces.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/src/boto3_assist/dynamodb/dynamodb_reindexer.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/src/boto3_assist/dynamodb/dynamodb_reserved_words.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/src/boto3_assist/dynamodb/dynamodb_reserved_words.txt +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/src/boto3_assist/dynamodb/readme.md +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/src/boto3_assist/dynamodb/troubleshooting.md +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/src/boto3_assist/ec2/ec2_connection.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/src/boto3_assist/environment_services/__init__.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/src/boto3_assist/environment_services/environment_loader.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/src/boto3_assist/environment_services/environment_variables.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/src/boto3_assist/utilities/datetime_utility.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/src/boto3_assist/utilities/logging_utility.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/src/boto3_assist/utilities/serialization_utility.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/src/boto3_assist/utilities/string_utility.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/tests/__init__.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/tests/__top/__init__.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/tests/dynamodb/__init__.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/tests/dynamodb/dynamodb_model_base_test.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/tests/dynamodb/dynamodb_model_projections_test.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/tests/dynamodb/dynamodb_model_serializtion_test.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/tests/dynamodb/dynamodb_moto_sorting_test.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/tests/dynamodb/dynamodb_reindex_test.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/tests/dynamodb/models/cms/base.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/tests/dynamodb/models/cms/content_block.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/tests/dynamodb/models/cms/page.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/tests/dynamodb/models/cms/template.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/tests/dynamodb/models/simple_model.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/tests/examples_test/__init__.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/tests/examples_test/user_service_test.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/tests/utilities/__init__.py +0 -0
- {boto3_assist-0.2.0 → boto3_assist-0.2.2}/tests/utilities/serialization_utility_test.py +0 -0
{boto3_assist-0.2.0 → boto3_assist-0.2.2}/src/boto3_assist/cloudwatch/cloudwatch_connection.py
RENAMED
|
@@ -8,10 +8,8 @@ from typing import Optional
|
|
|
8
8
|
from typing import TYPE_CHECKING
|
|
9
9
|
|
|
10
10
|
from aws_lambda_powertools import Logger
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
EnvironmentVariables,
|
|
14
|
-
)
|
|
11
|
+
|
|
12
|
+
|
|
15
13
|
from boto3_assist.cloudwatch.cloudwatch_connection_tracker import (
|
|
16
14
|
CloudWatchConnectionTracker,
|
|
17
15
|
)
|
|
@@ -144,7 +144,7 @@ class DynamoDB(DynamoDBConnection):
|
|
|
144
144
|
expression_attribute_names: Optional[dict] = None,
|
|
145
145
|
source: Optional[str] = None,
|
|
146
146
|
call_type: str = "resource",
|
|
147
|
-
) ->
|
|
147
|
+
) -> Dict[str, Any]: ...
|
|
148
148
|
|
|
149
149
|
@overload
|
|
150
150
|
def get(
|
|
@@ -158,7 +158,7 @@ class DynamoDB(DynamoDBConnection):
|
|
|
158
158
|
expression_attribute_names: Optional[dict] = None,
|
|
159
159
|
source: Optional[str] = None,
|
|
160
160
|
call_type: str = "resource",
|
|
161
|
-
) ->
|
|
161
|
+
) -> Dict[str, Any]: ...
|
|
162
162
|
|
|
163
163
|
@tracer.capture_method
|
|
164
164
|
def get(
|
|
@@ -98,7 +98,7 @@ class DynamoDBConnection:
|
|
|
98
98
|
return self.__session
|
|
99
99
|
|
|
100
100
|
@property
|
|
101
|
-
def
|
|
101
|
+
def client(self) -> DynamoDBClient:
|
|
102
102
|
"""DynamoDB Client Connection"""
|
|
103
103
|
if self.__dynamodb_client is None:
|
|
104
104
|
logger.info("Creating DynamoDB Client")
|
|
@@ -108,13 +108,26 @@ class DynamoDBConnection:
|
|
|
108
108
|
raise RuntimeError("DynamoDB Client is not available")
|
|
109
109
|
return self.__dynamodb_client
|
|
110
110
|
|
|
111
|
+
@client.setter
|
|
112
|
+
def client(self, value: DynamoDBClient):
|
|
113
|
+
logger.info("Setting DynamoDB Client")
|
|
114
|
+
self.__dynamodb_client = value
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def dynamodb_client(self) -> DynamoDBClient:
|
|
118
|
+
"""
|
|
119
|
+
DynamoDB Client Connection
|
|
120
|
+
- Backward Compatible. You should use client instead
|
|
121
|
+
"""
|
|
122
|
+
return self.client
|
|
123
|
+
|
|
111
124
|
@dynamodb_client.setter
|
|
112
125
|
def dynamodb_client(self, value: DynamoDBClient):
|
|
113
126
|
logger.info("Setting DynamoDB Client")
|
|
114
127
|
self.__dynamodb_client = value
|
|
115
128
|
|
|
116
129
|
@property
|
|
117
|
-
def
|
|
130
|
+
def resource(self) -> DynamoDBServiceResource:
|
|
118
131
|
"""DynamoDB Resource Connection"""
|
|
119
132
|
if self.__dynamodb_resource is None:
|
|
120
133
|
logger.info("Creating DynamoDB Resource")
|
|
@@ -125,6 +138,19 @@ class DynamoDBConnection:
|
|
|
125
138
|
|
|
126
139
|
return self.__dynamodb_resource
|
|
127
140
|
|
|
141
|
+
@resource.setter
|
|
142
|
+
def resource(self, value: DynamoDBServiceResource):
|
|
143
|
+
logger.info("Setting DynamoDB Resource")
|
|
144
|
+
self.__dynamodb_resource = value
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def dynamodb_resource(self) -> DynamoDBServiceResource:
|
|
148
|
+
"""
|
|
149
|
+
DynamoDB Resource Connection
|
|
150
|
+
- Backward Compatible. You should use resource instead
|
|
151
|
+
"""
|
|
152
|
+
return self.resource
|
|
153
|
+
|
|
128
154
|
@dynamodb_resource.setter
|
|
129
155
|
def dynamodb_resource(self, value: DynamoDBServiceResource):
|
|
130
156
|
logger.info("Setting DynamoDB Resource")
|
|
@@ -47,14 +47,18 @@ class DynamoDBIndexes:
|
|
|
47
47
|
for _, v in self.__indexes.items():
|
|
48
48
|
if v.partition_key.attribute_name == index.partition_key.attribute_name:
|
|
49
49
|
raise ValueError(
|
|
50
|
-
f"
|
|
50
|
+
f"The attrubute {index.partition_key.attribute_name} is already being used by index "
|
|
51
|
+
f"{v.name}. "
|
|
52
|
+
f"Reusing this attribute would over write the value on index {v.name}"
|
|
51
53
|
)
|
|
52
54
|
# check if the gsi1.sort_key.attribute_name exists
|
|
53
55
|
if index.sort_key is not None:
|
|
54
56
|
for _, v in self.__indexes.items():
|
|
55
57
|
if v.sort_key.attribute_name == index.sort_key.attribute_name:
|
|
56
58
|
raise ValueError(
|
|
57
|
-
f"
|
|
59
|
+
f"The attrubute {index.sort_key.attribute_name} is already being used by index "
|
|
60
|
+
f"{v.name}. "
|
|
61
|
+
f"Reusing this attribute would over write the value on index {v.name}"
|
|
58
62
|
)
|
|
59
63
|
|
|
60
64
|
self.__indexes[index.name] = index
|
|
@@ -9,8 +9,7 @@ import datetime as dt
|
|
|
9
9
|
import decimal
|
|
10
10
|
import inspect
|
|
11
11
|
import uuid
|
|
12
|
-
import
|
|
13
|
-
from typing import TypeVar, List
|
|
12
|
+
from typing import TypeVar, List, Dict, Any
|
|
14
13
|
from boto3.dynamodb.types import TypeSerializer
|
|
15
14
|
from boto3_assist.utilities.serialization_utility import Serialization
|
|
16
15
|
from boto3_assist.dynamodb.dynamodb_helpers import DynamoDBHelpers
|
|
@@ -126,7 +125,7 @@ class DynamoDBModelBase:
|
|
|
126
125
|
def projection_expression_attribute_names(self, value: dict | None):
|
|
127
126
|
self.__projection_expression_attribute_names = value
|
|
128
127
|
|
|
129
|
-
def map(self: T, item:
|
|
128
|
+
def map(self: T, item: Dict[str, Any] | DynamoDBModelBase | None) -> T | None:
|
|
130
129
|
"""
|
|
131
130
|
Map the item to the instance. If the item is a DynamoDBModelBase,
|
|
132
131
|
it will be converted to a dictionary first and then mapped.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
class Error(Exception):
|
|
2
|
+
"""Base class for exceptions in this module."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class DbFailures(Error):
|
|
6
|
+
"""DB Failure Error"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class InvalidHttpMethod(Exception):
|
|
10
|
+
"""Invalid Http Method"""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
code=422,
|
|
15
|
+
message="Invalid Http Method",
|
|
16
|
+
):
|
|
17
|
+
"""The user account is not valid"""
|
|
18
|
+
self.message = {
|
|
19
|
+
"status_code": code,
|
|
20
|
+
"message": message,
|
|
21
|
+
}
|
|
22
|
+
super().__init__(self.message)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class InvalidRoutePath(Exception):
|
|
26
|
+
"""Invalid Http Route"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, message="Invalid Route"):
|
|
29
|
+
"""Invalid Route"""
|
|
30
|
+
self.message = {
|
|
31
|
+
"status_code": 404,
|
|
32
|
+
"message": message,
|
|
33
|
+
}
|
|
34
|
+
super().__init__(self.message)
|
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Geek Cafe, LLC
|
|
3
|
+
Maintainers: Eric Wilson
|
|
4
|
+
MIT License. See Project Root for the license information.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import tempfile
|
|
9
|
+
import time
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
from aws_lambda_powertools import Logger
|
|
13
|
+
from botocore.exceptions import ClientError
|
|
14
|
+
|
|
15
|
+
from boto3_assist.errors.custom_exceptions import InvalidHttpMethod
|
|
16
|
+
from boto3_assist.s3.s3_connection import S3Connection
|
|
17
|
+
from boto3_assist.utilities.datetime_utility import DatetimeUtility
|
|
18
|
+
from boto3_assist.utilities.file_operations import FileOperations
|
|
19
|
+
from boto3_assist.utilities.http_utility import HttpUtility
|
|
20
|
+
|
|
21
|
+
logger = Logger(child=True)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class S3(S3Connection):
|
|
25
|
+
"""Common S3 Actions"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
*,
|
|
30
|
+
aws_profile: Optional[str] = None,
|
|
31
|
+
aws_region: Optional[str] = None,
|
|
32
|
+
aws_end_point_url: Optional[str] = None,
|
|
33
|
+
aws_access_key_id: Optional[str] = None,
|
|
34
|
+
aws_secret_access_key: Optional[str] = None,
|
|
35
|
+
) -> None:
|
|
36
|
+
"""_summary_
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
aws_profile (Optional[str], optional): _description_. Defaults to None.
|
|
40
|
+
aws_region (Optional[str], optional): _description_. Defaults to None.
|
|
41
|
+
aws_end_point_url (Optional[str], optional): _description_. Defaults to None.
|
|
42
|
+
aws_access_key_id (Optional[str], optional): _description_. Defaults to None.
|
|
43
|
+
aws_secret_access_key (Optional[str], optional): _description_. Defaults to None.
|
|
44
|
+
"""
|
|
45
|
+
super().__init__(
|
|
46
|
+
aws_profile=aws_profile,
|
|
47
|
+
aws_region=aws_region,
|
|
48
|
+
aws_end_point_url=aws_end_point_url,
|
|
49
|
+
aws_access_key_id=aws_access_key_id,
|
|
50
|
+
aws_secret_access_key=aws_secret_access_key,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def generate_presigned_url(
|
|
54
|
+
self,
|
|
55
|
+
bucket_name: str,
|
|
56
|
+
key_path: str,
|
|
57
|
+
user_id: str,
|
|
58
|
+
file_name: str,
|
|
59
|
+
meta_data: dict | None = None,
|
|
60
|
+
expiration=3600,
|
|
61
|
+
method_type="POST",
|
|
62
|
+
) -> Dict[str, Any]:
|
|
63
|
+
"""
|
|
64
|
+
Create a signed URL for uploading a file to S3.
|
|
65
|
+
:param bucket_name: The name of the S3 bucket.
|
|
66
|
+
:param user_id: The user ID of the user uploading the file.
|
|
67
|
+
:param file_name: The file name of the file being uploaded.
|
|
68
|
+
:param aws_profile: The name of the AWS profile to use.
|
|
69
|
+
:param aws_region: The name of the AWS region to use.
|
|
70
|
+
:param expiration: The number of seconds the URL is valid for.
|
|
71
|
+
:return: The signed URL.
|
|
72
|
+
"""
|
|
73
|
+
start = DatetimeUtility.get_utc_now()
|
|
74
|
+
logger.debug(
|
|
75
|
+
f"Creating signed URL for bucket {bucket_name} for user {user_id} and file {file_name} at {start} UTC"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
file_extension = FileOperations.get_file_extension(file_name)
|
|
79
|
+
|
|
80
|
+
local_meta = {
|
|
81
|
+
"user_id": f"{user_id}",
|
|
82
|
+
"file_name": f"{file_name}",
|
|
83
|
+
"extension": f"{file_extension}",
|
|
84
|
+
"method": "pre-signed-upload",
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if not meta_data:
|
|
88
|
+
meta_data = local_meta
|
|
89
|
+
else:
|
|
90
|
+
meta_data.update(local_meta)
|
|
91
|
+
|
|
92
|
+
key = key_path
|
|
93
|
+
method_type = method_type.upper()
|
|
94
|
+
|
|
95
|
+
signed_url: str | Dict[str, Any]
|
|
96
|
+
if method_type == "PUT":
|
|
97
|
+
signed_url = self.client.generate_presigned_url(
|
|
98
|
+
"put_object",
|
|
99
|
+
Params={
|
|
100
|
+
"Bucket": f"{bucket_name}",
|
|
101
|
+
"Key": f"{key}",
|
|
102
|
+
# NOTE: if you include the ContentType or Metadata then its required in the when they upload the file
|
|
103
|
+
# Otherwise you will get a `SignatureDoesNotMatch` error
|
|
104
|
+
# for now I'm commenting it out.
|
|
105
|
+
#'ContentType': 'application/octet-stream',
|
|
106
|
+
#'ACL': 'private',
|
|
107
|
+
# "Metadata": meta_data,
|
|
108
|
+
},
|
|
109
|
+
ExpiresIn=expiration, # URL is valid for x seconds
|
|
110
|
+
)
|
|
111
|
+
elif method_type == "POST":
|
|
112
|
+
signed_url = self.client.generate_presigned_post(
|
|
113
|
+
bucket_name,
|
|
114
|
+
key,
|
|
115
|
+
ExpiresIn=expiration, # URL is valid for x seconds
|
|
116
|
+
)
|
|
117
|
+
elif method_type == "GET":
|
|
118
|
+
signed_url = self.client.generate_presigned_url(
|
|
119
|
+
"get_object",
|
|
120
|
+
Params={
|
|
121
|
+
"Bucket": f"{bucket_name}",
|
|
122
|
+
"Key": f"{key}",
|
|
123
|
+
},
|
|
124
|
+
ExpiresIn=expiration, # URL is valid for x seconds
|
|
125
|
+
)
|
|
126
|
+
else:
|
|
127
|
+
raise InvalidHttpMethod(
|
|
128
|
+
f'Unknown method type was referenced. valid types are "PUT", "POST", "GET" , "{method_type}" as used '
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
end = DatetimeUtility.get_utc_now()
|
|
132
|
+
logger.debug(f"Signed URL created in {end-start}")
|
|
133
|
+
|
|
134
|
+
response = {
|
|
135
|
+
"signed_url": signed_url,
|
|
136
|
+
"key": key,
|
|
137
|
+
"meta_data": meta_data,
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return response
|
|
141
|
+
|
|
142
|
+
def upload_file_obj(self, bucket: str, key: str, file_obj: bytes) -> str:
|
|
143
|
+
"""
|
|
144
|
+
Uploads a file object to s3. Returns the full s3 path s3://<bucket>/<key>
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
if key.startswith("/"):
|
|
148
|
+
# remove the first slash
|
|
149
|
+
key = key[1:]
|
|
150
|
+
|
|
151
|
+
logger.debug(
|
|
152
|
+
{
|
|
153
|
+
"metric_filter": "upload_file_to_s3",
|
|
154
|
+
"bucket": bucket,
|
|
155
|
+
"key": key,
|
|
156
|
+
}
|
|
157
|
+
)
|
|
158
|
+
try:
|
|
159
|
+
self.client.upload_fileobj(Fileobj=file_obj, Bucket=bucket, Key=key)
|
|
160
|
+
|
|
161
|
+
except ClientError as ce:
|
|
162
|
+
error = {
|
|
163
|
+
"metric_filter": "upload_file_to_s3_failure",
|
|
164
|
+
"s3 upload": "failure",
|
|
165
|
+
"bucket": bucket,
|
|
166
|
+
"key": key,
|
|
167
|
+
}
|
|
168
|
+
logger.error(error)
|
|
169
|
+
raise RuntimeError(error) from ce
|
|
170
|
+
|
|
171
|
+
return f"s3://{bucket}/{key}"
|
|
172
|
+
|
|
173
|
+
def upload_file(
|
|
174
|
+
self,
|
|
175
|
+
bucket: str,
|
|
176
|
+
key: str,
|
|
177
|
+
local_file_path: str,
|
|
178
|
+
throw_error_on_failure: bool = False,
|
|
179
|
+
) -> str | None:
|
|
180
|
+
"""
|
|
181
|
+
Uploads a file to s3. Returns the full s3 path s3://<bucket>/<key>
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
if key.startswith("/"):
|
|
185
|
+
# remove the first slash
|
|
186
|
+
key = key[1:]
|
|
187
|
+
|
|
188
|
+
# build the path
|
|
189
|
+
s3_path = f"s3://{bucket}/{key}"
|
|
190
|
+
|
|
191
|
+
logger.debug(
|
|
192
|
+
{
|
|
193
|
+
"metric_filter": "upload_file_to_s3",
|
|
194
|
+
"bucket": bucket,
|
|
195
|
+
"key": key,
|
|
196
|
+
"local_file_path": local_file_path,
|
|
197
|
+
}
|
|
198
|
+
)
|
|
199
|
+
try:
|
|
200
|
+
self.client.upload_file(local_file_path, bucket, key)
|
|
201
|
+
|
|
202
|
+
except ClientError as ce:
|
|
203
|
+
error = {
|
|
204
|
+
"metric_filter": "upload_file_to_s3_failure",
|
|
205
|
+
"s3 upload": "failure",
|
|
206
|
+
"bucket": bucket,
|
|
207
|
+
"key": key,
|
|
208
|
+
"local_file_path": local_file_path,
|
|
209
|
+
}
|
|
210
|
+
logger.error(error)
|
|
211
|
+
|
|
212
|
+
if throw_error_on_failure:
|
|
213
|
+
raise RuntimeError(error) from ce
|
|
214
|
+
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
return s3_path
|
|
218
|
+
|
|
219
|
+
def download_file(
|
|
220
|
+
self,
|
|
221
|
+
bucket: str,
|
|
222
|
+
key: str,
|
|
223
|
+
local_directory: str | None = None,
|
|
224
|
+
local_file_path: str | None = None,
|
|
225
|
+
retry_attempts: int = 3,
|
|
226
|
+
retry_sleep: int = 5,
|
|
227
|
+
) -> str:
|
|
228
|
+
"""Download a file from s3"""
|
|
229
|
+
exception: Exception | None = None
|
|
230
|
+
|
|
231
|
+
if retry_attempts == 0:
|
|
232
|
+
retry_attempts = 1
|
|
233
|
+
|
|
234
|
+
for i in range(retry_attempts):
|
|
235
|
+
exception = None
|
|
236
|
+
try:
|
|
237
|
+
path = self.download_file_no_retries(
|
|
238
|
+
bucket=bucket,
|
|
239
|
+
key=key,
|
|
240
|
+
local_directory=local_directory,
|
|
241
|
+
local_file_path=local_file_path,
|
|
242
|
+
)
|
|
243
|
+
if path and os.path.exists(path):
|
|
244
|
+
return path
|
|
245
|
+
|
|
246
|
+
except Exception as e: # pylint: disable=w0718
|
|
247
|
+
logger.warning(
|
|
248
|
+
{
|
|
249
|
+
"action": "download_file",
|
|
250
|
+
"result": "failure",
|
|
251
|
+
"exception": str(e),
|
|
252
|
+
"attempt": i + 1,
|
|
253
|
+
"retry_attempts": retry_attempts,
|
|
254
|
+
}
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
exception = e
|
|
258
|
+
|
|
259
|
+
# sleep for a bit
|
|
260
|
+
attempt = i + 1
|
|
261
|
+
time.sleep(attempt * retry_sleep)
|
|
262
|
+
|
|
263
|
+
if exception:
|
|
264
|
+
logger.exception(
|
|
265
|
+
{
|
|
266
|
+
"action": "download_file",
|
|
267
|
+
"result": "failure",
|
|
268
|
+
"exception": str(exception),
|
|
269
|
+
"retry_attempts": retry_attempts,
|
|
270
|
+
}
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
raise exception from exception
|
|
274
|
+
|
|
275
|
+
raise RuntimeError("Unable to download file")
|
|
276
|
+
|
|
277
|
+
def download_file_no_retries(
|
|
278
|
+
self,
|
|
279
|
+
bucket: str,
|
|
280
|
+
key: str,
|
|
281
|
+
local_directory: str | None = None,
|
|
282
|
+
local_file_path: str | None = None,
|
|
283
|
+
) -> str:
|
|
284
|
+
"""
|
|
285
|
+
Downloads a file from s3
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
bucket (str): s3 bucket
|
|
289
|
+
key (str): the s3 object key
|
|
290
|
+
local_directory (str, optional): Local directory to download to. Defaults to None.
|
|
291
|
+
If None, we'll use a local tmp directory.
|
|
292
|
+
|
|
293
|
+
Raises:
|
|
294
|
+
e:
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
str: Path to the downloaded file.
|
|
298
|
+
"""
|
|
299
|
+
|
|
300
|
+
decoded_object_key: str
|
|
301
|
+
try:
|
|
302
|
+
logger.debug(
|
|
303
|
+
{
|
|
304
|
+
"action": "downloading file",
|
|
305
|
+
"bucket": bucket,
|
|
306
|
+
"key": key,
|
|
307
|
+
"local_directory": local_directory,
|
|
308
|
+
}
|
|
309
|
+
)
|
|
310
|
+
return self.__download_file(bucket, key, local_directory, local_file_path)
|
|
311
|
+
except FileNotFoundError:
|
|
312
|
+
logger.warning(
|
|
313
|
+
{
|
|
314
|
+
"metric_filter": "download_file_error",
|
|
315
|
+
"error": "FileNotFoundError",
|
|
316
|
+
"message": "attempting to find it decoded",
|
|
317
|
+
"bucket": bucket,
|
|
318
|
+
"key": key,
|
|
319
|
+
}
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# attempt to decode the key
|
|
323
|
+
decoded_object_key = HttpUtility.decode_url(key)
|
|
324
|
+
|
|
325
|
+
logger.error(
|
|
326
|
+
{
|
|
327
|
+
"metric_filter": "download_file_error",
|
|
328
|
+
"error": "FileNotFoundError",
|
|
329
|
+
"message": "attempting to find it decoded",
|
|
330
|
+
"bucket": bucket,
|
|
331
|
+
"key": key,
|
|
332
|
+
"decoded_object_key": decoded_object_key,
|
|
333
|
+
}
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
return self.__download_file(bucket, decoded_object_key, local_directory)
|
|
337
|
+
|
|
338
|
+
except Exception as e:
|
|
339
|
+
logger.error(
|
|
340
|
+
{
|
|
341
|
+
"metric_filter": "download_file_error",
|
|
342
|
+
"error": str(e),
|
|
343
|
+
"bucket": bucket,
|
|
344
|
+
"decoded_object_key": decoded_object_key,
|
|
345
|
+
}
|
|
346
|
+
)
|
|
347
|
+
raise e
|
|
348
|
+
|
|
349
|
+
def stream_file(self, bucket_name: str, key: str) -> Dict[str, Any]:
|
|
350
|
+
"""
|
|
351
|
+
Gets a file from s3 and returns the response.
|
|
352
|
+
The "Body" is a streaming body object. You can read it like a file.
|
|
353
|
+
For example:
|
|
354
|
+
|
|
355
|
+
with response["Body"] as f:
|
|
356
|
+
data = f.read()
|
|
357
|
+
print(data)
|
|
358
|
+
|
|
359
|
+
"""
|
|
360
|
+
|
|
361
|
+
logger.debug(
|
|
362
|
+
{
|
|
363
|
+
"source": "download_file",
|
|
364
|
+
"action": "downloading a file from s3",
|
|
365
|
+
"bucket": bucket_name,
|
|
366
|
+
"key": key,
|
|
367
|
+
}
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
response: Dict[str, Any] = {}
|
|
371
|
+
error = None
|
|
372
|
+
|
|
373
|
+
try:
|
|
374
|
+
response = dict(self.client.get_object(Bucket=bucket_name, Key=key))
|
|
375
|
+
|
|
376
|
+
logger.debug(
|
|
377
|
+
{"metric_filter": "s3_download_response", "response": str(response)}
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
except Exception as e: # pylint: disable=W0718
|
|
381
|
+
error = str(e)
|
|
382
|
+
logger.error({"metric_filter": "s3_download_error", "error": str(e)})
|
|
383
|
+
raise RuntimeError(
|
|
384
|
+
{
|
|
385
|
+
"metric_filter": "s3_download_error",
|
|
386
|
+
"error": str(e),
|
|
387
|
+
"bucket": bucket_name,
|
|
388
|
+
"key": key,
|
|
389
|
+
}
|
|
390
|
+
) from e
|
|
391
|
+
|
|
392
|
+
finally:
|
|
393
|
+
logger.debug(
|
|
394
|
+
{
|
|
395
|
+
"source": "download_file",
|
|
396
|
+
"action": "downloading a file from s3",
|
|
397
|
+
"bucket": bucket_name,
|
|
398
|
+
"key": key,
|
|
399
|
+
"response": response,
|
|
400
|
+
"errors": error,
|
|
401
|
+
}
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
return response
|
|
405
|
+
|
|
406
|
+
def __download_file(
|
|
407
|
+
self,
|
|
408
|
+
bucket: str,
|
|
409
|
+
key: str,
|
|
410
|
+
local_directory: str | None = None,
|
|
411
|
+
local_file_path: str | None = None,
|
|
412
|
+
):
|
|
413
|
+
if local_directory and local_file_path:
|
|
414
|
+
raise ValueError(
|
|
415
|
+
"Only one of local_directory or local_file_path can be provided"
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
if local_directory and not os.path.exists(local_directory):
|
|
419
|
+
FileOperations.makedirs(local_directory)
|
|
420
|
+
|
|
421
|
+
if local_file_path and not os.path.exists(os.path.dirname(local_file_path)):
|
|
422
|
+
FileOperations.makedirs(os.path.dirname(local_file_path))
|
|
423
|
+
|
|
424
|
+
file_name = self.__get_file_name_from_path(key)
|
|
425
|
+
if local_directory is None and local_file_path is None:
|
|
426
|
+
local_path = self.get_local_path_for_file(file_name)
|
|
427
|
+
elif local_directory:
|
|
428
|
+
local_path = os.path.join(local_directory, file_name)
|
|
429
|
+
else:
|
|
430
|
+
local_path = local_file_path
|
|
431
|
+
|
|
432
|
+
logger.debug(
|
|
433
|
+
{
|
|
434
|
+
"source": "download_file",
|
|
435
|
+
"action": "downloading a file from s3",
|
|
436
|
+
"bucket": bucket,
|
|
437
|
+
"key": key,
|
|
438
|
+
"file_name": file_name,
|
|
439
|
+
"local_path": local_path,
|
|
440
|
+
}
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
error: str | None = None
|
|
444
|
+
try:
|
|
445
|
+
self.client.download_file(bucket, key, local_path)
|
|
446
|
+
|
|
447
|
+
except Exception as e: # pylint: disable=W0718
|
|
448
|
+
error = str(e)
|
|
449
|
+
logger.error({"metric_filter": "s3_download_error", "error": str(e)})
|
|
450
|
+
|
|
451
|
+
file_exist = os.path.exists(local_path)
|
|
452
|
+
|
|
453
|
+
logger.debug(
|
|
454
|
+
{
|
|
455
|
+
"source": "download_file",
|
|
456
|
+
"action": "downloading a file from s3",
|
|
457
|
+
"bucket": bucket,
|
|
458
|
+
"key": key,
|
|
459
|
+
"file_name": file_name,
|
|
460
|
+
"local_path": local_path,
|
|
461
|
+
"file_downloaded": file_exist,
|
|
462
|
+
"errors": error,
|
|
463
|
+
}
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
if not file_exist:
|
|
467
|
+
raise FileNotFoundError("File Failed to download (does not exist) from S3.")
|
|
468
|
+
|
|
469
|
+
return local_path
|
|
470
|
+
|
|
471
|
+
def __get_file_name_from_path(self, path: str) -> str:
|
|
472
|
+
"""
|
|
473
|
+
Get a file name from the path
|
|
474
|
+
|
|
475
|
+
Args:
|
|
476
|
+
path (str): a file path
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
str: the file name
|
|
480
|
+
"""
|
|
481
|
+
return path.rsplit("/")[-1]
|
|
482
|
+
|
|
483
|
+
def get_local_path_for_file(self, file_name: str):
|
|
484
|
+
"""
|
|
485
|
+
Get a local temp location for a file.
|
|
486
|
+
This is designed to work with lambda functions.
|
|
487
|
+
The /tmp directory is the only writeable location for lambda functions.
|
|
488
|
+
"""
|
|
489
|
+
temp_dir = self.get_temp_directory()
|
|
490
|
+
# use /tmp it's the only writeable location for lambda
|
|
491
|
+
local_path = os.path.join(temp_dir, file_name)
|
|
492
|
+
return local_path
|
|
493
|
+
|
|
494
|
+
def get_temp_directory(self):
|
|
495
|
+
"""
|
|
496
|
+
Determines the appropriate temporary directory based on the environment.
|
|
497
|
+
If running in AWS Lambda, returns '/tmp'.
|
|
498
|
+
Otherwise, returns the system's standard temp directory.
|
|
499
|
+
"""
|
|
500
|
+
if "AWS_LAMBDA_FUNCTION_NAME" in os.environ:
|
|
501
|
+
# In AWS Lambda environment
|
|
502
|
+
return "/tmp"
|
|
503
|
+
else:
|
|
504
|
+
# Not in AWS Lambda, use the system's default temp directory
|
|
505
|
+
return tempfile.gettempdir()
|