boto3-assist 0.6.1__tar.gz → 0.8.0__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.
Files changed (121) hide show
  1. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/PKG-INFO +1 -1
  2. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/pyproject.toml +1 -1
  3. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/connection_tracker.py +4 -4
  4. boto3_assist-0.8.0/src/boto3_assist/s3/s3.py +64 -0
  5. boto3_assist-0.8.0/src/boto3_assist/s3/s3_bucket.py +67 -0
  6. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/s3/s3_connection.py +1 -4
  7. boto3_assist-0.6.1/src/boto3_assist/s3/s3.py → boto3_assist-0.8.0/src/boto3_assist/s3/s3_object.py +109 -47
  8. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/utilities/serialization_utility.py +18 -7
  9. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/utilities/string_utility.py +30 -0
  10. boto3_assist-0.8.0/src/boto3_assist/version.py +1 -0
  11. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/tests/__top/__init__.py +12 -6
  12. boto3_assist-0.8.0/tests/s3/s3_file_delete_test.py +123 -0
  13. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/tests/s3/s3_file_upload_test.py +8 -6
  14. boto3_assist-0.8.0/tests/utilities/string_utility_test.py +56 -0
  15. boto3_assist-0.6.1/src/boto3_assist/version.py +0 -1
  16. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/.env.docker +0 -0
  17. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/.env.docker.001 +0 -0
  18. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/.env.docker.nosql.workbench +0 -0
  19. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/.env.unittest +0 -0
  20. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/.gitignore +0 -0
  21. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/.vscode/launch.json +0 -0
  22. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/.vscode/settings.json +0 -0
  23. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/.vscode/tasks.json +0 -0
  24. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/LICENSE-EXPLAINED.txt +0 -0
  25. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/LICENSE.txt +0 -0
  26. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/README.md +0 -0
  27. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/aws_regions_with_status.csv +0 -0
  28. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/aws_regions_with_status.json +0 -0
  29. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/devops/build.py +0 -0
  30. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/devops/readme.md +0 -0
  31. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/examples/__init__.py +0 -0
  32. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/examples/cloudwatch/log_report.py +0 -0
  33. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/examples/dynamodb/models/order_item_model.py +0 -0
  34. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/examples/dynamodb/models/order_model.py +0 -0
  35. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/examples/dynamodb/models/product_model.py +0 -0
  36. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/examples/dynamodb/models/user_model.py +0 -0
  37. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/examples/dynamodb/models/user_post_model.py +0 -0
  38. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/examples/dynamodb/order_example/main.py +0 -0
  39. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/examples/dynamodb/order_example/products.json +0 -0
  40. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/examples/dynamodb/services/order_item_service.py +0 -0
  41. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/examples/dynamodb/services/order_service.py +0 -0
  42. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/examples/dynamodb/services/product_service.py +0 -0
  43. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/examples/dynamodb/services/table_service.py +0 -0
  44. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/examples/dynamodb/services/user_post_service.py +0 -0
  45. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/examples/dynamodb/services/user_service.py +0 -0
  46. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/examples/dynamodb/services/user_service_client_example.py +0 -0
  47. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/examples/dynamodb/services/user_service_resource_example.py +0 -0
  48. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/examples/dynamodb/user_post_example/main.py +0 -0
  49. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/examples/ec2/regions_report.py +0 -0
  50. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/module-headers.txt +0 -0
  51. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/mypy.ini +0 -0
  52. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/requirements-dev.txt +0 -0
  53. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/requirements.txt +0 -0
  54. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/run-checks.sh +0 -0
  55. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/run_unit_tests.sh +0 -0
  56. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/__init__.py +0 -0
  57. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/aws_lambda/event_info.py +0 -0
  58. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/boto3session.py +0 -0
  59. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/cloudwatch/cloudwatch_connection.py +0 -0
  60. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/cloudwatch/cloudwatch_connection_tracker.py +0 -0
  61. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/cloudwatch/cloudwatch_log_connection.py +0 -0
  62. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/cloudwatch/cloudwatch_logs.py +0 -0
  63. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/cloudwatch/cloudwatch_query.py +0 -0
  64. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/cognito/cognito_authorizer.py +0 -0
  65. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/cognito/cognito_connection.py +0 -0
  66. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/cognito/cognito_utility.py +0 -0
  67. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/cognito/jwks_cache.py +0 -0
  68. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/cognito/user.py +0 -0
  69. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/connection.py +0 -0
  70. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/dynamodb/dynamodb.py +0 -0
  71. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/dynamodb/dynamodb_connection.py +0 -0
  72. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/dynamodb/dynamodb_helpers.py +0 -0
  73. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/dynamodb/dynamodb_importer.py +0 -0
  74. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/dynamodb/dynamodb_index.py +0 -0
  75. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/dynamodb/dynamodb_iservice.py +0 -0
  76. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/dynamodb/dynamodb_key.py +0 -0
  77. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/dynamodb/dynamodb_model_base.py +0 -0
  78. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/dynamodb/dynamodb_model_base_interfaces.py +0 -0
  79. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/dynamodb/dynamodb_reindexer.py +0 -0
  80. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/dynamodb/dynamodb_reserved_words.py +0 -0
  81. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/dynamodb/dynamodb_reserved_words.txt +0 -0
  82. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/dynamodb/readme.md +0 -0
  83. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/dynamodb/troubleshooting.md +0 -0
  84. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/ec2/ec2_connection.py +0 -0
  85. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/environment_services/__init__.py +0 -0
  86. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/environment_services/environment_loader.py +0 -0
  87. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/environment_services/environment_variables.py +0 -0
  88. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/errors/custom_exceptions.py +0 -0
  89. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/http_status_codes.py +0 -0
  90. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/models/serializable_model.py +0 -0
  91. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/utilities/datetime_utility.py +0 -0
  92. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/utilities/dictionaroy_utility.py +0 -0
  93. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/utilities/file_operations.py +0 -0
  94. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/utilities/http_utility.py +0 -0
  95. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/utilities/logging_utility.py +0 -0
  96. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/src/boto3_assist/utilities/numbers_utility.py +0 -0
  97. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/tests/__init__.py +0 -0
  98. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/tests/dynamodb/__init__.py +0 -0
  99. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/tests/dynamodb/dbmodels/cms/base.py +0 -0
  100. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/tests/dynamodb/dbmodels/cms/content_block.py +0 -0
  101. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/tests/dynamodb/dbmodels/cms/page.py +0 -0
  102. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/tests/dynamodb/dbmodels/cms/template.py +0 -0
  103. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/tests/dynamodb/dbmodels/simple_model.py +0 -0
  104. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/tests/dynamodb/dbmodels/user_model.py +0 -0
  105. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/tests/dynamodb/dbmodels/user_required_fields_model.py +0 -0
  106. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/tests/dynamodb/dynamodb_model_base_test.py +0 -0
  107. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/tests/dynamodb/dynamodb_model_projections_test.py +0 -0
  108. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/tests/dynamodb/dynamodb_model_serializtion_test.py +0 -0
  109. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/tests/dynamodb/dynamodb_moto_sorting_test.py +0 -0
  110. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/tests/dynamodb/dynamodb_reindex_test.py +0 -0
  111. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/tests/examples_test/__init__.py +0 -0
  112. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/tests/examples_test/user_service_test.py +0 -0
  113. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/tests/lambda/__init__.py +0 -0
  114. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/tests/lambda/event_info_test.py +0 -0
  115. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/tests/models/__init__.py +0 -0
  116. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/tests/models/serializable_model_test.py +0 -0
  117. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/tests/models/serializable_model_wide_test.py +0 -0
  118. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/tests/s3/__init__.py +0 -0
  119. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/tests/s3/files/test.txt +0 -0
  120. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/tests/utilities/__init__.py +0 -0
  121. {boto3_assist-0.6.1 → boto3_assist-0.8.0}/tests/utilities/serialization_utility_test.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boto3_assist
3
- Version: 0.6.1
3
+ Version: 0.8.0
4
4
  Summary: Additional boto3 wrappers to make your life a little easier
5
5
  Author-email: Eric Wilson <boto3-assist@geekcafe.com>
6
6
  License-File: LICENSE-EXPLAINED.txt
@@ -7,7 +7,7 @@ packages = ["src/boto3_assist"]
7
7
 
8
8
  [project]
9
9
  name = "boto3_assist"
10
- version = "0.6.1"
10
+ version = "0.8.0"
11
11
 
12
12
  authors = [
13
13
  { name="Eric Wilson", email="boto3-assist@geekcafe.com" }
@@ -67,8 +67,8 @@ class ConnectionTracker:
67
67
 
68
68
  if not self.issue_stack_trace:
69
69
  stack_trace_message = (
70
- f"\nTo add additional information to the log and determine where additional connections are being created, "
71
- f"set the environment variable {self.__stack_trace_env_var} to true.\n"
70
+ f"📄 NOTE: To add additional information 👀 to the log and determine where additional connections are being created: "
71
+ f"set the environment variable 👉{self.__stack_trace_env_var}👈 to true ✅. \n"
72
72
  )
73
73
  else:
74
74
  stack = "\n".join(traceback.format_stack())
@@ -83,8 +83,8 @@ class ConnectionTracker:
83
83
  "instead of creating a new one. Connections are expensive in terms of time and latency. "
84
84
  "If you are seeing performance issues, check how and where you are creating your "
85
85
  "connections. You should be able to pass the connection to your other objects "
86
- "and reuse your boto3 connections."
87
- "\n\nMOCK Testing may show this message as well, in which case you can dismiss this warning.\n\n"
86
+ "and reuse your boto3 connections. "
87
+ "\n🧪 MOCK Testing may show this message as well, in which case you can dismiss this warning.🧪\n"
88
88
  f"{stack_trace_message}"
89
89
  )
90
90
 
@@ -0,0 +1,64 @@
1
+ """
2
+ Geek Cafe, LLC
3
+ Maintainers: Eric Wilson
4
+ MIT License. See Project Root for the license information.
5
+ """
6
+
7
+ from typing import Optional, cast
8
+
9
+ from aws_lambda_powertools import Logger
10
+
11
+ from boto3_assist.s3.s3_connection import S3Connection
12
+ from boto3_assist.s3.s3_object import S3Object
13
+ from boto3_assist.s3.s3_bucket import S3Bucket
14
+
15
+ logger = Logger(child=True)
16
+
17
+
18
+ class S3(S3Connection):
19
+ """Common S3 Actions"""
20
+
21
+ def __init__(
22
+ self,
23
+ *,
24
+ aws_profile: Optional[str] = None,
25
+ aws_region: Optional[str] = None,
26
+ aws_end_point_url: Optional[str] = None,
27
+ aws_access_key_id: Optional[str] = None,
28
+ aws_secret_access_key: Optional[str] = None,
29
+ ) -> None:
30
+ """_summary_
31
+
32
+ Args:
33
+ aws_profile (Optional[str], optional): _description_. Defaults to None.
34
+ aws_region (Optional[str], optional): _description_. Defaults to None.
35
+ aws_end_point_url (Optional[str], optional): _description_. Defaults to None.
36
+ aws_access_key_id (Optional[str], optional): _description_. Defaults to None.
37
+ aws_secret_access_key (Optional[str], optional): _description_. Defaults to None.
38
+ """
39
+ super().__init__(
40
+ aws_profile=aws_profile,
41
+ aws_region=aws_region,
42
+ aws_end_point_url=aws_end_point_url,
43
+ aws_access_key_id=aws_access_key_id,
44
+ aws_secret_access_key=aws_secret_access_key,
45
+ )
46
+
47
+ self.__s3_object: S3Object | None = None
48
+ self.__s3_bucket: S3Bucket | None = None
49
+
50
+ @property
51
+ def object(self) -> S3Object:
52
+ """s3 object"""
53
+ if self.__s3_object is None:
54
+ connection = cast(S3Connection, self)
55
+ self.__s3_object = S3Object(connection)
56
+ return self.__s3_object
57
+
58
+ @property
59
+ def bucket(self) -> S3Bucket:
60
+ """s3 bucket"""
61
+ if self.__s3_bucket is None:
62
+ connection = cast(S3Connection, self)
63
+ self.__s3_bucket = S3Bucket(connection)
64
+ return self.__s3_bucket
@@ -0,0 +1,67 @@
1
+ """
2
+ Geek Cafe, LLC
3
+ Maintainers: Eric Wilson
4
+ MIT License. See Project Root for the license information.
5
+ """
6
+
7
+ from typing import Any, Dict
8
+
9
+ from aws_lambda_powertools import Logger
10
+ from botocore.exceptions import ClientError
11
+
12
+
13
+ from boto3_assist.s3.s3_connection import S3Connection
14
+
15
+ logger = Logger(child=True)
16
+
17
+
18
+ class S3Bucket:
19
+ """Common S3 Actions"""
20
+
21
+ def __init__(self, connection: S3Connection):
22
+ self.connection = connection or S3Connection()
23
+
24
+ def create(self, *, bucket_name: str) -> Dict[str, Any]:
25
+ """
26
+ Create an S3 bucket
27
+ :param bucket_name: Bucket to create
28
+ :return: True if bucket is created, else False
29
+ """
30
+ try:
31
+ response = self.connection.client.create_bucket(Bucket=bucket_name)
32
+ logger.info(f"Bucket {bucket_name} created")
33
+
34
+ return dict(response)
35
+ except ClientError as e:
36
+ logger.exception(e)
37
+ raise e
38
+
39
+ def enable_versioning(self, *, bucket_name: str) -> None:
40
+ """
41
+ Enable versioning on an S3 bucket
42
+ :param bucket_name: Bucket to enable versioning on
43
+ :return: None
44
+ """
45
+ try:
46
+ self.connection.client.put_bucket_versioning(
47
+ Bucket=bucket_name, VersioningConfiguration={"Status": "Enabled"}
48
+ )
49
+ logger.info(f"Versioning enabled on bucket {bucket_name}")
50
+ except ClientError as e:
51
+ logger.exception(e)
52
+ raise e
53
+
54
+ def disable_versioning(self, *, bucket_name: str) -> None:
55
+ """
56
+ Disable versioning on an S3 bucket
57
+ :param bucket_name: Bucket to disable versioning on
58
+ :return: None
59
+ """
60
+ try:
61
+ self.connection.client.put_bucket_versioning(
62
+ Bucket=bucket_name, VersioningConfiguration={"Status": "Suspended"}
63
+ )
64
+ logger.info(f"Versioning disabled on bucket {bucket_name}")
65
+ except ClientError as e:
66
+ logger.exception(e)
67
+ raise e
@@ -8,10 +8,7 @@ from typing import Optional
8
8
  from typing import TYPE_CHECKING
9
9
 
10
10
  from aws_lambda_powertools import Logger
11
- from boto3_assist.boto3session import Boto3SessionManager
12
- from boto3_assist.environment_services.environment_variables import (
13
- EnvironmentVariables,
14
- )
11
+
15
12
  from boto3_assist.connection import Connection
16
13
 
17
14
  if TYPE_CHECKING:
@@ -8,7 +8,7 @@ import os
8
8
  import tempfile
9
9
  import time
10
10
  import io
11
- from typing import Any, Dict, List, Optional
11
+ from typing import Any, Dict, Optional, List
12
12
 
13
13
  from aws_lambda_powertools import Logger
14
14
  from botocore.exceptions import ClientError
@@ -19,61 +19,100 @@ from boto3_assist.utilities.datetime_utility import DatetimeUtility
19
19
  from boto3_assist.utilities.file_operations import FileOperations
20
20
  from boto3_assist.utilities.http_utility import HttpUtility
21
21
 
22
+
22
23
  logger = Logger(child=True)
23
24
 
24
25
 
25
- class S3(S3Connection):
26
- """Common S3 Actions"""
26
+ class S3Object:
27
+ """S3 Object Actions"""
27
28
 
28
- def __init__(
29
- self,
30
- *,
31
- aws_profile: Optional[str] = None,
32
- aws_region: Optional[str] = None,
33
- aws_end_point_url: Optional[str] = None,
34
- aws_access_key_id: Optional[str] = None,
35
- aws_secret_access_key: Optional[str] = None,
36
- ) -> None:
37
- """_summary_
29
+ def __init__(self, connection: S3Connection):
30
+ self.connection = connection or S3Connection()
38
31
 
39
- Args:
40
- aws_profile (Optional[str], optional): _description_. Defaults to None.
41
- aws_region (Optional[str], optional): _description_. Defaults to None.
42
- aws_end_point_url (Optional[str], optional): _description_. Defaults to None.
43
- aws_access_key_id (Optional[str], optional): _description_. Defaults to None.
44
- aws_secret_access_key (Optional[str], optional): _description_. Defaults to None.
32
+ def delete(self, *, bucket_name: str, key: str) -> Dict[str, Any]:
45
33
  """
46
- super().__init__(
47
- aws_profile=aws_profile,
48
- aws_region=aws_region,
49
- aws_end_point_url=aws_end_point_url,
50
- aws_access_key_id=aws_access_key_id,
51
- aws_secret_access_key=aws_secret_access_key,
52
- )
34
+ Deletes an object key
53
35
 
54
- def create_bucket(self, *, bucket_name: str) -> None:
55
- """
56
- Create an S3 bucket
57
- :param bucket_name: Bucket to create
58
- :return: True if bucket is created, else False
36
+ Args:
37
+ bucket_name (str): The AWS Bucket Name
38
+ key (str): The Object Key
59
39
  """
40
+ s3 = self.connection.client
41
+ # see if the object exists
60
42
  try:
61
- self.client.create_bucket(Bucket=bucket_name)
62
- logger.info(f"Bucket {bucket_name} created")
63
- except ClientError as e:
64
- logger.exception(e)
65
- raise e
43
+ response = s3.head_object(Bucket=bucket_name, Key=key)
44
+ response = s3.delete_object(Bucket=bucket_name, Key=key)
45
+ except s3.exceptions.NoSuchKey:
46
+ response = {"ResponseMetadata": {"HTTPStatusCode": 404}}
47
+ except s3.exceptions.ClientError as e:
48
+ if e.response.get("Error", {}).get("Code") == "404":
49
+ response = {"ResponseMetadata": {"HTTPStatusCode": 404}}
50
+ else:
51
+ raise e
52
+
53
+ return dict(response)
54
+
55
+ def delete_all_versions(
56
+ self, *, bucket_name: str, key: str, include_deleted: bool = False
57
+ ) -> List[str]:
58
+ """
59
+ Deletes an object key and all the versions for that object key
60
+
61
+ Args:
62
+ bucket_name (str): The AWS Bucket Name
63
+ key (str): The Object Kuye
64
+ include_deleted (bool, optional): Should deleted files be removed as well.
65
+ If True it will look for the object keys with the deleted marker and remove it.
66
+ Defaults to False.
67
+ """
68
+ s3 = self.connection.client
69
+ paginator = s3.get_paginator("list_object_versions")
70
+ files: List[str] = []
71
+
72
+ for page in paginator.paginate(Bucket=bucket_name, Prefix=key):
73
+ # Delete object versions
74
+ if "Versions" in page:
75
+ for version in page["Versions"]:
76
+ s3.delete_object(
77
+ Bucket=bucket_name,
78
+ Key=version["Key"],
79
+ VersionId=version["VersionId"],
80
+ )
81
+
82
+ files.append(f"{version['Key']} - {version['VersionId']}")
83
+
84
+ if include_deleted:
85
+ # delete a previous files that may have just been a soft delete.
86
+ if "DeleteMarkers" in page:
87
+ for marker in page["DeleteMarkers"]:
88
+ s3.delete_object(
89
+ Bucket=bucket_name,
90
+ Key=marker["Key"],
91
+ VersionId=marker["VersionId"],
92
+ )
93
+
94
+ files.append(
95
+ f"{marker['Key']}:{marker['VersionId']}:delete-marker"
96
+ )
97
+ else:
98
+ response = self.delete(bucket_name=bucket_name, key=key)
99
+ if response["ResponseMetadata"]["HTTPStatusCode"] == 404:
100
+ return files
101
+
102
+ files.append(key)
103
+
104
+ return files
66
105
 
67
106
  def generate_presigned_url(
68
107
  self,
69
108
  *,
70
109
  bucket_name: str,
71
110
  key_path: str,
72
- user_id: str,
73
111
  file_name: str,
74
- meta_data: dict | None = None,
75
- expiration=3600,
76
- method_type="POST",
112
+ meta_data: Optional[dict] = None,
113
+ expiration: int = 3600,
114
+ method_type: str = "POST",
115
+ user_id: Optional[str] = None,
77
116
  ) -> Dict[str, Any]:
78
117
  """
79
118
  Create a signed URL for uploading a file to S3.
@@ -109,7 +148,7 @@ class S3(S3Connection):
109
148
 
110
149
  signed_url: str | Dict[str, Any]
111
150
  if method_type == "PUT":
112
- signed_url = self.client.generate_presigned_url(
151
+ signed_url = self.connection.client.generate_presigned_url(
113
152
  "put_object",
114
153
  Params={
115
154
  "Bucket": f"{bucket_name}",
@@ -124,13 +163,13 @@ class S3(S3Connection):
124
163
  ExpiresIn=expiration, # URL is valid for x seconds
125
164
  )
126
165
  elif method_type == "POST":
127
- signed_url = self.client.generate_presigned_post(
166
+ signed_url = self.connection.client.generate_presigned_post(
128
167
  bucket_name,
129
168
  key,
130
169
  ExpiresIn=expiration, # URL is valid for x seconds
131
170
  )
132
171
  elif method_type == "GET":
133
- signed_url = self.client.generate_presigned_url(
172
+ signed_url = self.connection.client.generate_presigned_url(
134
173
  "get_object",
135
174
  Params={
136
175
  "Bucket": f"{bucket_name}",
@@ -175,7 +214,7 @@ class S3(S3Connection):
175
214
  file_obj: bytes = (
176
215
  file_obj.encode("utf-8") if isinstance(file_obj, str) else file_obj
177
216
  )
178
- self.client.upload_fileobj(
217
+ self.connection.client.upload_fileobj(
179
218
  Fileobj=io.BytesIO(file_obj), Bucket=bucket, Key=key
180
219
  )
181
220
 
@@ -219,7 +258,7 @@ class S3(S3Connection):
219
258
  }
220
259
  )
221
260
  try:
222
- self.client.upload_file(local_file_path, bucket, key)
261
+ self.connection.client.upload_file(local_file_path, bucket, key)
223
262
 
224
263
  except ClientError as ce:
225
264
  error = {
@@ -394,7 +433,9 @@ class S3(S3Connection):
394
433
  error = None
395
434
 
396
435
  try:
397
- response = dict(self.client.get_object(Bucket=bucket_name, Key=key))
436
+ response = dict(
437
+ self.connection.client.get_object(Bucket=bucket_name, Key=key)
438
+ )
398
439
 
399
440
  logger.debug(
400
441
  {"metric_filter": "s3_download_response", "response": str(response)}
@@ -465,7 +506,7 @@ class S3(S3Connection):
465
506
 
466
507
  error: str | None = None
467
508
  try:
468
- self.client.download_file(bucket, key, local_path)
509
+ self.connection.client.download_file(bucket, key, local_path)
469
510
 
470
511
  except Exception as e: # pylint: disable=W0718
471
512
  error = str(e)
@@ -542,3 +583,24 @@ class S3(S3Connection):
542
583
  Decodes bytes to a string
543
584
  """
544
585
  return file_obj.decode(encoding=encoding, errors=errors)
586
+
587
+ def list_versions(self, bucket: str, prefix: str = "") -> List[str]:
588
+ """
589
+ List all versions of objects in an S3 bucket with a given prefix.
590
+
591
+ Args:
592
+ bucket (str): The name of the S3 bucket.
593
+ prefix (str, optional): The prefix to filter objects by. Defaults to "".
594
+
595
+ Returns:
596
+ list: A list of dictionaries containing information about each object version.
597
+ """
598
+ versions = []
599
+ paginator = self.connection.client.get_paginator("list_object_versions")
600
+ page_iterator = paginator.paginate(Bucket=bucket, Prefix=prefix)
601
+
602
+ for page in page_iterator:
603
+ if "Versions" in page:
604
+ versions.extend(page["Versions"])
605
+
606
+ return versions
@@ -308,19 +308,30 @@ class Serialization:
308
308
  )
309
309
  raise
310
310
  except Exception as e: # pylint: disable=w0718
311
- logger.error(
312
- f"Error setting attribute {key} with value {value}: {e}. "
313
- "This usually occurs on properties that don't have setters. "
314
- "You can add a setter (even with a pass action) for this property, "
315
- "decorate it with the @exclude_from_serialization "
316
- "or ignore this error. "
317
- )
311
+ if not Serialization.has_setter(target, key):
312
+ logger.warning(
313
+ f"Error warning attempting to set attribute {key} with value {value}: {e}. "
314
+ "This usually occurs on properties that don't have setters. "
315
+ "You should add a setter (even with a pass action) for this property or "
316
+ "decorate it with the @exclude_from_serialization to avoid this warning."
317
+ )
318
+ else:
319
+ raise e
318
320
 
319
321
  if hasattr(target, "__actively_serializing_data__"):
320
322
  setattr(target, "__actively_serializing_data__", False)
321
323
 
322
324
  return target
323
325
 
326
+ @staticmethod
327
+ def has_setter(obj: object, attr_name: str) -> bool:
328
+ """Check if the given attribute has a setter defined."""
329
+ cls = obj.__class__
330
+ if not hasattr(cls, attr_name):
331
+ return False
332
+ attr = getattr(cls, attr_name, None)
333
+ return isinstance(attr, property) and attr.fset is not None
334
+
324
335
  @staticmethod
325
336
  def has_attribute(obj: object, attribute_name: str) -> bool:
326
337
  """Check if an object has an attribute"""
@@ -195,6 +195,36 @@ class StringUtility:
195
195
  hash_object.update(encoded_string)
196
196
  return hash_object.hexdigest()
197
197
 
198
+ @staticmethod
199
+ def generate_idempotent_uuid(
200
+ namespace: uuid.UUID | str, unique_string: str, case_sensitive: bool = False
201
+ ) -> str:
202
+ """
203
+ Generates an idempotnent UUID, which is useful for creates
204
+
205
+ Args:
206
+ namespace (GUID | str): A namespace for your id, it must be a UUID or a string in a UUID format
207
+ unique_string (str): A unique string like an email address, a tenant name.
208
+ Use a combination for more granularity:
209
+ tenant-name:email
210
+ vendor:product-name
211
+ vendor:product-id
212
+ etc
213
+
214
+ Returns:
215
+ str: a string representation of a UUID
216
+ """
217
+ if isinstance(namespace, str):
218
+ namespace = uuid.UUID(namespace)
219
+
220
+ if not unique_string:
221
+ raise ValueError("unique_string cannot be empty")
222
+
223
+ if not case_sensitive:
224
+ unique_string = unique_string.lower()
225
+
226
+ return str(uuid.uuid5(namespace, unique_string))
227
+
198
228
  @staticmethod
199
229
  def get_size_in_kb(input_string: str | dict) -> float:
200
230
  """
@@ -0,0 +1 @@
1
+ __version__ = '0.8.0'
@@ -8,12 +8,18 @@ import os
8
8
  import sys
9
9
  from pathlib import Path
10
10
 
11
- ## needed for discovery based top level execution
12
- print("👋 init test paths for __top")
11
+ VERBOSE: bool = os.getenv("VERBOSE") or False
12
+
13
+ if VERBOSE:
14
+ print("👋 init test paths for __top")
15
+
16
+
13
17
  root_directory = Path(__file__).resolve().parent.parent.parent
14
18
  src_directory = os.path.join(root_directory, "src")
15
-
19
+ # inject src path to python search path
16
20
  sys.path.insert(0, src_directory)
17
- print("")
18
- for p in sys.path:
19
- print(f"👉 {p}")
21
+
22
+ if VERBOSE:
23
+ print("")
24
+ for p in sys.path:
25
+ print(f"👉 {p}")
@@ -0,0 +1,123 @@
1
+ import os
2
+ import unittest
3
+ from pathlib import Path
4
+
5
+ import moto
6
+
7
+ from boto3_assist.environment_services.environment_loader import EnvironmentLoader
8
+ from boto3_assist.s3.s3 import S3
9
+
10
+
11
+ @moto.mock_aws
12
+ class S3FileDeleteTest(unittest.TestCase):
13
+ """Test S3 File Upload"""
14
+
15
+ def setUp(self):
16
+ """Setup"""
17
+ ev: EnvironmentLoader = EnvironmentLoader()
18
+ # NOTE: you need to make sure the the env file below exists or you will get an error
19
+ ev.load_environment_file(file_name=".env.unittest")
20
+
21
+ def test_delete_file(self):
22
+ """Test uploading a file"""
23
+ s3 = S3()
24
+ bucket_name: str = "test-bucket"
25
+ s3.bucket.create(bucket_name=bucket_name)
26
+ s3.bucket.enable_versioning(bucket_name=bucket_name)
27
+ local_file_path: Path = Path(
28
+ os.path.join(os.path.dirname(__file__), "files", "test.txt")
29
+ )
30
+ if not os.path.exists(local_file_path):
31
+ raise FileNotFoundError(f"File not found: {local_file_path}")
32
+
33
+ for _ in range(0, 5):
34
+ # upload the same file over and over, it should create different versions
35
+ s3.object.upload_file(
36
+ bucket=bucket_name, key="test.txt", local_file_path=local_file_path
37
+ )
38
+
39
+ files = s3.object.delete_all_versions(bucket_name=bucket_name, key="test.txt")
40
+
41
+ self.assertEqual(len(files), 5)
42
+
43
+ def test_delete_file_including_delete_markers(self):
44
+ """Test uploading a file"""
45
+ s3 = S3()
46
+ bucket_name: str = "unittest-bucket"
47
+ test_file_name = "test.txt"
48
+ key = "test_with_delete.txt"
49
+ s3.bucket.create(bucket_name=bucket_name)
50
+ s3.bucket.enable_versioning(bucket_name=bucket_name)
51
+ local_file_path: Path = Path(
52
+ os.path.join(os.path.dirname(__file__), "files", test_file_name)
53
+ )
54
+ if not os.path.exists(local_file_path):
55
+ raise FileNotFoundError(f"File not found: {local_file_path}")
56
+
57
+ for _ in range(0, 5):
58
+ # upload the same file over and over, it should create different versions
59
+ s3.object.upload_file(
60
+ bucket=bucket_name, key=key, local_file_path=local_file_path
61
+ )
62
+
63
+ files = s3.object.delete_all_versions(
64
+ bucket_name=bucket_name, key=key, include_deleted=True
65
+ )
66
+
67
+ # we should have ten here, the original 5 and the deleted 5
68
+ self.assertEqual(len(files), 5)
69
+
70
+ files = s3.object.delete_all_versions(
71
+ bucket_name=bucket_name, key=key, include_deleted=True
72
+ )
73
+
74
+ # we shouldn't have any more files here
75
+ self.assertEqual(len(files), 0)
76
+
77
+ def test_delete_file_including_delete_markers_2(self):
78
+ """Test uploading a file"""
79
+ s3 = S3()
80
+ bucket_name: str = "unittest-bucket"
81
+ test_file_name = "test.txt"
82
+ key = "test_with_delete.txt"
83
+
84
+ s3.bucket.create(bucket_name=bucket_name)
85
+
86
+ s3.bucket.enable_versioning(bucket_name=bucket_name)
87
+ local_file_path: Path = Path(
88
+ os.path.join(os.path.dirname(__file__), "files", test_file_name)
89
+ )
90
+ if not os.path.exists(local_file_path):
91
+ raise FileNotFoundError(f"File not found: {local_file_path}")
92
+
93
+ for _ in range(0, 5):
94
+ # upload the same file over and over, it should create different versions
95
+ s3.object.upload_file(
96
+ bucket=bucket_name, key=key, local_file_path=local_file_path
97
+ )
98
+
99
+ # delete the latest which should add a deleted marker
100
+ s3.object.delete(bucket_name=bucket_name, key=key)
101
+
102
+ files = s3.object.delete_all_versions(
103
+ bucket_name=bucket_name, key=key, include_deleted=True
104
+ )
105
+
106
+ # we should have ten here, the original 5 and the 1 deleted marker
107
+ self.assertEqual(len(files), 6)
108
+
109
+ files = s3.object.delete_all_versions(
110
+ bucket_name=bucket_name, key=key, include_deleted=True
111
+ )
112
+
113
+ # we shouldn't have any more files here
114
+ self.assertEqual(len(files), 0)
115
+
116
+
117
+ def main():
118
+ """Main"""
119
+ unittest.main()
120
+
121
+
122
+ if __name__ == "__main__":
123
+ main()
@@ -1,9 +1,11 @@
1
1
  import os
2
2
  import unittest
3
- import moto
4
3
  from pathlib import Path
5
- from boto3_assist.s3.s3 import S3
4
+
5
+ import moto
6
+
6
7
  from boto3_assist.environment_services.environment_loader import EnvironmentLoader
8
+ from boto3_assist.s3.s3 import S3
7
9
  from boto3_assist.utilities.file_operations import FileOperations
8
10
 
9
11
 
@@ -21,14 +23,14 @@ class S3FileUploadTest(unittest.TestCase):
21
23
  """Test uploading a file"""
22
24
  s3 = S3()
23
25
  bucket_name: str = "test-bucket"
24
- s3.create_bucket(bucket_name=bucket_name)
26
+ s3.bucket.create(bucket_name=bucket_name)
25
27
  local_file_path: Path = Path(
26
28
  os.path.join(os.path.dirname(__file__), "files", "test.txt")
27
29
  )
28
30
  if not os.path.exists(local_file_path):
29
31
  raise FileNotFoundError(f"File not found: {local_file_path}")
30
32
 
31
- s3.upload_file(
33
+ s3.object.upload_file(
32
34
  bucket=bucket_name, key="test.txt", local_file_path=local_file_path
33
35
  )
34
36
 
@@ -36,7 +38,7 @@ class S3FileUploadTest(unittest.TestCase):
36
38
  """Test uploading a file"""
37
39
  s3 = S3()
38
40
  bucket_name: str = "test-bucket"
39
- s3.create_bucket(bucket_name=bucket_name)
41
+ s3.bucket.create(bucket_name=bucket_name)
40
42
  local_file_path: Path = Path(
41
43
  os.path.join(os.path.dirname(__file__), "files", "test.txt")
42
44
  )
@@ -45,4 +47,4 @@ class S3FileUploadTest(unittest.TestCase):
45
47
 
46
48
  data = FileOperations.read_file(local_file_path)
47
49
 
48
- s3.upload_file_obj(bucket=bucket_name, key="test.txt", file_obj=data)
50
+ s3.object.upload_file_obj(bucket=bucket_name, key="test.txt", file_obj=data)
@@ -0,0 +1,56 @@
1
+ """
2
+ Geek Cafe, LLC
3
+ Maintainers: Eric Wilson
4
+ MIT License. See Project Root for the license information.
5
+ """
6
+
7
+ import unittest
8
+ from datetime import datetime, UTC
9
+ from datetime import timedelta
10
+ from typing import cast
11
+
12
+ from boto3_assist.utilities.string_utility import StringUtility
13
+ import uuid
14
+
15
+
16
+ class StringUtilityUnitTest(unittest.TestCase):
17
+ "String Utility Tests"
18
+
19
+ def test_uuid_idempotency(self):
20
+ """Testing Idempotnent UUID generation."""
21
+ # must be consistent
22
+ namespace: uuid.UUID = uuid.UUID("6ba7b810-9dad-11d1-80b4-00c04fd430c8")
23
+
24
+ idempotent_id_john_smith: str = StringUtility.generate_idempotent_uuid(
25
+ str(namespace), "tenant-one:john.smith@tenant-one.com"
26
+ )
27
+
28
+ # should always get this GUID
29
+ self.assertEqual(
30
+ idempotent_id_john_smith, "3e68597f-3a32-5f82-a028-010f34eccfe6"
31
+ )
32
+
33
+ idempotent_id_john_smith_camel: str = StringUtility.generate_idempotent_uuid(
34
+ namespace, "tenant-one:John.Smith@tenant-one.com", case_sensitive=True
35
+ )
36
+
37
+ # should always get this GUID
38
+ self.assertEqual(
39
+ idempotent_id_john_smith_camel, "06daa067-77e1-56c1-80d5-8ee8306d0298"
40
+ )
41
+
42
+ idempotent_id_john_smith_case_insensitive: str = (
43
+ StringUtility.generate_idempotent_uuid(
44
+ namespace,
45
+ "tenant-one:John.Smith@tenant-one.com",
46
+ case_sensitive=False,
47
+ )
48
+ )
49
+ self.assertEqual(
50
+ idempotent_id_john_smith_case_insensitive,
51
+ "3e68597f-3a32-5f82-a028-010f34eccfe6",
52
+ )
53
+
54
+ self.assertEqual(
55
+ idempotent_id_john_smith, idempotent_id_john_smith_case_insensitive
56
+ )
@@ -1 +0,0 @@
1
- __version__ = '0.6.1'
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes