boto3-assist 0.6.0__tar.gz → 0.7.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 (122) hide show
  1. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/.gitignore +2 -0
  2. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/.vscode/settings.json +1 -2
  3. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/PKG-INFO +1 -1
  4. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/pyproject.toml +1 -1
  5. boto3_assist-0.7.0/run_unit_tests.sh +22 -0
  6. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/connection_tracker.py +4 -4
  7. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/models/serializable_model.py +2 -2
  8. boto3_assist-0.7.0/src/boto3_assist/s3/s3.py +64 -0
  9. boto3_assist-0.7.0/src/boto3_assist/s3/s3_bucket.py +67 -0
  10. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/s3/s3_connection.py +1 -4
  11. boto3_assist-0.6.0/src/boto3_assist/s3/s3.py → boto3_assist-0.7.0/src/boto3_assist/s3/s3_object.py +109 -47
  12. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/utilities/serialization_utility.py +131 -11
  13. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/utilities/string_utility.py +30 -0
  14. boto3_assist-0.7.0/src/boto3_assist/version.py +1 -0
  15. {boto3_assist-0.6.0/tests/utilities → boto3_assist-0.7.0/tests/__top}/__init__.py +12 -3
  16. {boto3_assist-0.6.0/tests/dynamodb/models → boto3_assist-0.7.0/tests/dynamodb/dbmodels}/cms/page.py +1 -1
  17. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/tests/dynamodb/dynamodb_model_base_test.py +3 -4
  18. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/tests/dynamodb/dynamodb_model_projections_test.py +2 -2
  19. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/tests/dynamodb/dynamodb_model_serializtion_test.py +2 -2
  20. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/tests/dynamodb/dynamodb_moto_sorting_test.py +3 -5
  21. boto3_assist-0.7.0/tests/models/serializable_model_wide_test.py +246 -0
  22. boto3_assist-0.7.0/tests/s3/s3_file_delete_test.py +123 -0
  23. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/tests/s3/s3_file_upload_test.py +8 -6
  24. boto3_assist-0.7.0/tests/utilities/__init__.py +0 -0
  25. boto3_assist-0.7.0/tests/utilities/string_utility_test.py +56 -0
  26. boto3_assist-0.6.0/src/boto3_assist/version.py +0 -1
  27. boto3_assist-0.6.0/tests/__top/__init__.py +0 -16
  28. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/.env.docker +0 -0
  29. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/.env.docker.001 +0 -0
  30. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/.env.docker.nosql.workbench +0 -0
  31. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/.env.unittest +0 -0
  32. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/.vscode/launch.json +0 -0
  33. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/.vscode/tasks.json +0 -0
  34. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/LICENSE-EXPLAINED.txt +0 -0
  35. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/LICENSE.txt +0 -0
  36. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/README.md +0 -0
  37. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/aws_regions_with_status.csv +0 -0
  38. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/aws_regions_with_status.json +0 -0
  39. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/devops/build.py +0 -0
  40. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/devops/readme.md +0 -0
  41. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/examples/__init__.py +0 -0
  42. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/examples/cloudwatch/log_report.py +0 -0
  43. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/examples/dynamodb/models/order_item_model.py +0 -0
  44. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/examples/dynamodb/models/order_model.py +0 -0
  45. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/examples/dynamodb/models/product_model.py +0 -0
  46. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/examples/dynamodb/models/user_model.py +0 -0
  47. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/examples/dynamodb/models/user_post_model.py +0 -0
  48. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/examples/dynamodb/order_example/main.py +0 -0
  49. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/examples/dynamodb/order_example/products.json +0 -0
  50. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/examples/dynamodb/services/order_item_service.py +0 -0
  51. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/examples/dynamodb/services/order_service.py +0 -0
  52. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/examples/dynamodb/services/product_service.py +0 -0
  53. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/examples/dynamodb/services/table_service.py +0 -0
  54. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/examples/dynamodb/services/user_post_service.py +0 -0
  55. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/examples/dynamodb/services/user_service.py +0 -0
  56. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/examples/dynamodb/services/user_service_client_example.py +0 -0
  57. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/examples/dynamodb/services/user_service_resource_example.py +0 -0
  58. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/examples/dynamodb/user_post_example/main.py +0 -0
  59. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/examples/ec2/regions_report.py +0 -0
  60. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/module-headers.txt +0 -0
  61. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/mypy.ini +0 -0
  62. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/requirements-dev.txt +0 -0
  63. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/requirements.txt +0 -0
  64. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/run-checks.sh +0 -0
  65. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/__init__.py +0 -0
  66. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/aws_lambda/event_info.py +0 -0
  67. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/boto3session.py +0 -0
  68. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/cloudwatch/cloudwatch_connection.py +0 -0
  69. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/cloudwatch/cloudwatch_connection_tracker.py +0 -0
  70. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/cloudwatch/cloudwatch_log_connection.py +0 -0
  71. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/cloudwatch/cloudwatch_logs.py +0 -0
  72. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/cloudwatch/cloudwatch_query.py +0 -0
  73. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/cognito/cognito_authorizer.py +0 -0
  74. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/cognito/cognito_connection.py +0 -0
  75. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/cognito/cognito_utility.py +0 -0
  76. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/cognito/jwks_cache.py +0 -0
  77. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/cognito/user.py +0 -0
  78. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/connection.py +0 -0
  79. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/dynamodb/dynamodb.py +0 -0
  80. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/dynamodb/dynamodb_connection.py +0 -0
  81. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/dynamodb/dynamodb_helpers.py +0 -0
  82. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/dynamodb/dynamodb_importer.py +0 -0
  83. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/dynamodb/dynamodb_index.py +0 -0
  84. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/dynamodb/dynamodb_iservice.py +0 -0
  85. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/dynamodb/dynamodb_key.py +0 -0
  86. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/dynamodb/dynamodb_model_base.py +0 -0
  87. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/dynamodb/dynamodb_model_base_interfaces.py +0 -0
  88. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/dynamodb/dynamodb_reindexer.py +0 -0
  89. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/dynamodb/dynamodb_reserved_words.py +0 -0
  90. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/dynamodb/dynamodb_reserved_words.txt +0 -0
  91. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/dynamodb/readme.md +0 -0
  92. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/dynamodb/troubleshooting.md +0 -0
  93. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/ec2/ec2_connection.py +0 -0
  94. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/environment_services/__init__.py +0 -0
  95. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/environment_services/environment_loader.py +0 -0
  96. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/environment_services/environment_variables.py +0 -0
  97. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/errors/custom_exceptions.py +0 -0
  98. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/http_status_codes.py +0 -0
  99. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/utilities/datetime_utility.py +0 -0
  100. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/utilities/dictionaroy_utility.py +0 -0
  101. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/utilities/file_operations.py +0 -0
  102. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/utilities/http_utility.py +0 -0
  103. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/utilities/logging_utility.py +0 -0
  104. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/src/boto3_assist/utilities/numbers_utility.py +0 -0
  105. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/tests/__init__.py +0 -0
  106. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/tests/dynamodb/__init__.py +0 -0
  107. {boto3_assist-0.6.0/tests/dynamodb/models → boto3_assist-0.7.0/tests/dynamodb/dbmodels}/cms/base.py +0 -0
  108. {boto3_assist-0.6.0/tests/dynamodb/models → boto3_assist-0.7.0/tests/dynamodb/dbmodels}/cms/content_block.py +0 -0
  109. {boto3_assist-0.6.0/tests/dynamodb/models → boto3_assist-0.7.0/tests/dynamodb/dbmodels}/cms/template.py +0 -0
  110. {boto3_assist-0.6.0/tests/dynamodb/models → boto3_assist-0.7.0/tests/dynamodb/dbmodels}/simple_model.py +0 -0
  111. {boto3_assist-0.6.0/tests/dynamodb/models → boto3_assist-0.7.0/tests/dynamodb/dbmodels}/user_model.py +0 -0
  112. {boto3_assist-0.6.0/tests/dynamodb/models → boto3_assist-0.7.0/tests/dynamodb/dbmodels}/user_required_fields_model.py +0 -0
  113. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/tests/dynamodb/dynamodb_reindex_test.py +0 -0
  114. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/tests/examples_test/__init__.py +0 -0
  115. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/tests/examples_test/user_service_test.py +0 -0
  116. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/tests/lambda/__init__.py +0 -0
  117. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/tests/lambda/event_info_test.py +0 -0
  118. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/tests/models/__init__.py +0 -0
  119. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/tests/models/serializable_model_test.py +0 -0
  120. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/tests/s3/__init__.py +0 -0
  121. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/tests/s3/files/test.txt +0 -0
  122. {boto3_assist-0.6.0 → boto3_assist-0.7.0}/tests/utilities/serialization_utility_test.py +0 -0
@@ -163,3 +163,5 @@ cython_debug/
163
163
 
164
164
  .imports
165
165
  .env.development
166
+ .unittest
167
+ .unittests
@@ -12,8 +12,7 @@
12
12
  "python.analysis.extraPaths": [
13
13
  "${workspaceFolder}",
14
14
  "${workspaceFolder}/examples",
15
- "${workspaceFolder}/src",
16
- "${workspaceFolder}/src/boto3_assist",
15
+ "${workspaceFolder}/src",
17
16
  "${workspaceFolder}/tests",
18
17
  "${workspaceFolder}/devops",
19
18
  "${workspaceFolder}/devops/cdk",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boto3_assist
3
- Version: 0.6.0
3
+ Version: 0.7.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.0"
10
+ version = "0.7.0"
11
11
 
12
12
  authors = [
13
13
  { name="Eric Wilson", email="boto3-assist@geekcafe.com" }
@@ -0,0 +1,22 @@
1
+ #!/bin/bash
2
+
3
+ python --version
4
+ python -m venv .unittest
5
+ source ./.unittest/bin/activate
6
+ which python
7
+
8
+
9
+ pip install --upgrade pip
10
+ pip install -r ./requirements.txt
11
+ pip install -r ./requirements-dev.txt
12
+
13
+
14
+ echo "running unit test"
15
+ python -m unittest discover -s tests -p "*_test.py"
16
+
17
+ if [ $? -eq 0 ]; then
18
+ echo "Tests passed successfully"
19
+ else
20
+ echo "No tests found or tests failed"
21
+ exit 1
22
+ fi
@@ -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
 
@@ -5,5 +5,5 @@ MIT License. See Project Root for the license information.
5
5
  """
6
6
 
7
7
  from __future__ import annotations
8
- from typing import TypeVar, Dict, Any
9
- from boto3_assist.utilities.serialization_utility import SerializableModel
8
+
9
+ from boto3_assist.utilities.serialization_utility import SerializableModel # noqa: F401 # pylint: disable=unused-import
@@ -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
@@ -1,16 +1,15 @@
1
1
  """Serialization Utility"""
2
2
 
3
- from datetime import datetime
4
- from decimal import Decimal
5
- from typing import Dict, List, TypeVar, Any
6
- import json
7
- import jsons
8
3
  import datetime as dt
9
4
  import decimal
10
5
  import inspect
6
+ import json
11
7
  import uuid
12
- from aws_lambda_powertools import Logger
8
+ from datetime import datetime
9
+ from decimal import Decimal
10
+ from typing import Any, Dict, List, TypeVar
13
11
 
12
+ from aws_lambda_powertools import Logger
14
13
 
15
14
  T = TypeVar("T")
16
15
 
@@ -53,6 +52,15 @@ class SerializableModel:
53
52
  instance=self, serialize_fn=lambda x: x, include_none=True
54
53
  )
55
54
 
55
+ def to_wide_dictionary(self) -> Dict:
56
+ """
57
+ Dumps an object to dictionary structure
58
+ """
59
+
60
+ dump = Serialization.to_wide_dictionary(model=self)
61
+
62
+ return dump
63
+
56
64
 
57
65
  class JsonEncoder(json.JSONEncoder):
58
66
  """
@@ -108,6 +116,32 @@ class Serialization:
108
116
 
109
117
  return dump
110
118
 
119
+ @staticmethod
120
+ def to_wide_dictionary(model: object) -> Dict:
121
+ """
122
+ Dumps an object to dictionary structure
123
+ """
124
+
125
+ dump = Serialization.to_dict(
126
+ instance=model, serialize_fn=lambda x: x, include_none=True
127
+ )
128
+
129
+ # have a dictionary now let's flatten out
130
+ flat_dict = {}
131
+ for key, value in dump.items():
132
+ if isinstance(value, dict):
133
+ for sub_key, sub_value in value.items():
134
+ flat_dict[f"{key}_{sub_key}"] = sub_value
135
+ elif isinstance(value, list):
136
+ for i, sub_value in enumerate(value):
137
+ sub_dict = Serialization.to_wide_dictionary(sub_value)
138
+ for sub_key, sub_value in sub_dict.items():
139
+ flat_dict[f"{key}_{i}_{sub_key}"] = sub_value
140
+ else:
141
+ flat_dict[key] = value
142
+
143
+ return flat_dict
144
+
111
145
  @staticmethod
112
146
  def map(source: object, target: T, coerce: bool = True) -> T | None:
113
147
  """Map an object from one object to another"""
@@ -118,12 +152,92 @@ class Serialization:
118
152
  source_dict = Serialization.convert_object_to_dict(source)
119
153
  if not isinstance(source_dict, dict):
120
154
  return None
121
- return Serialization.load_properties(
155
+ return Serialization._load_properties(
122
156
  source=source_dict, target=target, coerce=coerce
123
157
  )
124
158
 
125
159
  @staticmethod
126
- def load_properties(
160
+ def to_wide_dictionary_list(
161
+ data: Dict[str, Any] | List[Dict[str, Any]],
162
+ remove_collisions: bool = True,
163
+ raise_error_on_collision: bool = False,
164
+ ) -> List[Dict[str, Any]]:
165
+ """
166
+ Converts a dictionary or list of dictionaries to a list of dictionaries.
167
+
168
+ :param data: Dictionary or list of dictionaries to be converted
169
+ :param remove_collisions: If True, removes duplicate keys from the dictionaries
170
+ :return: List of dictionaries
171
+ """
172
+
173
+ collisions = []
174
+
175
+ def recursive_flatten(prefix, obj):
176
+ """
177
+ Recursively flattens a JSON object.
178
+
179
+ :param prefix: Current key prefix
180
+ :param obj: Object to flatten
181
+ :return: List of flattened dictionaries
182
+ """
183
+ if isinstance(obj, list):
184
+ result = []
185
+ for _, item in enumerate(obj):
186
+ x = recursive_flatten("", item)
187
+ result.extend(x)
188
+ return result
189
+ elif isinstance(obj, dict):
190
+ result = [{}]
191
+ for key, value in obj.items():
192
+ sub_result = recursive_flatten(
193
+ f"{prefix}_{key}" if prefix else key, value
194
+ )
195
+ new_result = []
196
+ for entry in result:
197
+ for sub_entry in sub_result:
198
+ # remove any collisions
199
+
200
+ for k in entry:
201
+ if k in sub_entry:
202
+ if k not in collisions:
203
+ logger.debug(f"Collision detected: {k}")
204
+ collisions.append(k)
205
+ merged = entry.copy()
206
+ merged.update(sub_entry)
207
+ new_result.append(merged)
208
+ result = new_result
209
+ return result
210
+ else:
211
+ return [{prefix: obj}] if prefix else []
212
+
213
+ results = recursive_flatten("", data)
214
+ if remove_collisions:
215
+ results = Serialization.remove_collisions(results, collisions)
216
+
217
+ if raise_error_on_collision and len(collisions) > 0:
218
+ raise ValueError(f"Duplicate keys detected: {collisions}")
219
+
220
+ return results
221
+
222
+ @staticmethod
223
+ def remove_collisions(
224
+ data: List[Dict[str, Any]], collisions: List[str]
225
+ ) -> List[Dict[str, Any]]:
226
+ """
227
+ Removes collisions from a list of dictionaries.
228
+
229
+ :param data: List of dictionaries
230
+ :param collisions: List of collision keys
231
+ :return: List of dictionaries with collisions removed
232
+ """
233
+ for c in collisions:
234
+ for r in data:
235
+ if c in r:
236
+ del r[c]
237
+ return data
238
+
239
+ @staticmethod
240
+ def _load_properties(
127
241
  source: dict,
128
242
  target: T,
129
243
  coerce: bool = True,
@@ -183,9 +297,9 @@ class Serialization:
183
297
  attr.clear()
184
298
  attr.extend(value)
185
299
  elif isinstance(attr, dict) and isinstance(value, dict):
186
- Serialization.load_properties(value, attr, coerce=coerce)
300
+ Serialization._load_properties(value, attr, coerce=coerce)
187
301
  elif hasattr(attr, "__dict__") and isinstance(value, dict):
188
- Serialization.load_properties(value, attr, coerce=coerce)
302
+ Serialization._load_properties(value, attr, coerce=coerce)
189
303
  else:
190
304
  setattr(target, key, value)
191
305
  except ValueError as e:
@@ -230,12 +344,18 @@ class Serialization:
230
344
 
231
345
  @staticmethod
232
346
  def to_dict(
233
- instance: SerializableModel,
347
+ instance: SerializableModel | dict,
234
348
  serialize_fn,
235
349
  include_none: bool = True,
236
350
  ) -> Dict[str, Any]:
237
351
  """To Dict / Dictionary"""
238
352
 
353
+ if instance is None:
354
+ return {}
355
+
356
+ if isinstance(instance, dict):
357
+ return instance
358
+
239
359
  def is_primitive(value):
240
360
  """Check if the value is a primitive data type."""
241
361
  return isinstance(value, (str, int, bool, type(None)))