boto3-assist 0.1.14__tar.gz → 0.2.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 (95) hide show
  1. boto3_assist-0.2.0/.env.unittest +17 -0
  2. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/.gitignore +2 -1
  3. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/PKG-INFO +1 -3
  4. boto3_assist-0.2.0/examples/cloudwatch/log_report.py +62 -0
  5. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/examples/dynamodb/models/order_item_model.py +5 -1
  6. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/examples/dynamodb/order_example/main.py +6 -5
  7. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/examples/dynamodb/services/order_item_service.py +1 -1
  8. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/examples/dynamodb/user_post_example/main.py +1 -1
  9. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/examples/ec2/regions_report.py +1 -1
  10. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/pyproject.toml +1 -1
  11. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/requirements-dev.txt +1 -0
  12. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/src/boto3_assist/boto3session.py +3 -2
  13. boto3_assist-0.2.0/src/boto3_assist/cloudwatch/cloudwatch_connection.py +86 -0
  14. boto3_assist-0.2.0/src/boto3_assist/cloudwatch/cloudwatch_connection_tracker.py +17 -0
  15. boto3_assist-0.2.0/src/boto3_assist/cloudwatch/cloudwatch_log_connection.py +62 -0
  16. boto3_assist-0.2.0/src/boto3_assist/cloudwatch/cloudwatch_logs.py +39 -0
  17. boto3_assist-0.2.0/src/boto3_assist/cloudwatch/cloudwatch_query.py +191 -0
  18. boto3_assist-0.2.0/src/boto3_assist/connection.py +101 -0
  19. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/src/boto3_assist/connection_tracker.py +8 -8
  20. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/src/boto3_assist/dynamodb/dynamodb.py +28 -17
  21. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/src/boto3_assist/dynamodb/dynamodb_connection.py +3 -0
  22. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/src/boto3_assist/dynamodb/dynamodb_index.py +23 -0
  23. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/src/boto3_assist/dynamodb/dynamodb_iservice.py +4 -0
  24. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/src/boto3_assist/dynamodb/dynamodb_model_base.py +5 -5
  25. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/src/boto3_assist/dynamodb/dynamodb_reserved_words.py +6 -3
  26. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/src/boto3_assist/dynamodb/dynamodb_reserved_words.txt +0 -1
  27. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/src/boto3_assist/ec2/ec2_connection.py +3 -0
  28. boto3_assist-0.2.0/src/boto3_assist/environment_services/environment_loader.py +111 -0
  29. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/src/boto3_assist/environment_services/environment_variables.py +4 -0
  30. boto3_assist-0.2.0/src/boto3_assist/version.py +1 -0
  31. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/tests/dynamodb/dynamodb_model_serializtion_test.py +0 -1
  32. boto3_assist-0.2.0/tests/dynamodb/dynamodb_moto_sorting_test.py +159 -0
  33. boto3_assist-0.2.0/tests/dynamodb/models/cms/base.py +54 -0
  34. boto3_assist-0.2.0/tests/dynamodb/models/cms/content_block.py +53 -0
  35. boto3_assist-0.2.0/tests/dynamodb/models/cms/page.py +103 -0
  36. boto3_assist-0.2.0/tests/dynamodb/models/cms/template.py +59 -0
  37. boto3_assist-0.1.14/.env.development +0 -1
  38. boto3_assist-0.1.14/src/boto3_assist/environment_services/environment_loader.py +0 -47
  39. boto3_assist-0.1.14/src/boto3_assist/version.py +0 -1
  40. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/.env.docker +0 -0
  41. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/.env.docker.001 +0 -0
  42. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/.env.docker.nosql.workbench +0 -0
  43. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/.vscode/launch.json +0 -0
  44. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/.vscode/settings.json +0 -0
  45. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/.vscode/tasks.json +0 -0
  46. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/LICENSE-EXPLAINED.txt +0 -0
  47. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/LICENSE.txt +0 -0
  48. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/README.md +0 -0
  49. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/aws_regions_with_status.csv +0 -0
  50. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/aws_regions_with_status.json +0 -0
  51. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/devops/build.py +0 -0
  52. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/devops/readme.md +0 -0
  53. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/examples/__init__.py +0 -0
  54. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/examples/dynamodb/models/order_model.py +0 -0
  55. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/examples/dynamodb/models/product_model.py +0 -0
  56. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/examples/dynamodb/models/user_model.py +0 -0
  57. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/examples/dynamodb/models/user_post_model.py +0 -0
  58. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/examples/dynamodb/order_example/products.json +0 -0
  59. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/examples/dynamodb/services/order_service.py +0 -0
  60. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/examples/dynamodb/services/product_service.py +0 -0
  61. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/examples/dynamodb/services/table_service.py +0 -0
  62. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/examples/dynamodb/services/user_post_service.py +0 -0
  63. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/examples/dynamodb/services/user_service.py +0 -0
  64. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/examples/dynamodb/services/user_service_client_example.py +0 -0
  65. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/examples/dynamodb/services/user_service_resource_example.py +0 -0
  66. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/module-headers.txt +0 -0
  67. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/mypy.ini +0 -0
  68. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/requirements.txt +0 -0
  69. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/run-checks.sh +0 -0
  70. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/src/boto3_assist/__init__.py +0 -0
  71. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/src/boto3_assist/dynamodb/dynamodb_connection_tracker.py +0 -0
  72. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/src/boto3_assist/dynamodb/dynamodb_helpers.py +0 -0
  73. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/src/boto3_assist/dynamodb/dynamodb_importer.py +0 -0
  74. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/src/boto3_assist/dynamodb/dynamodb_key.py +0 -0
  75. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/src/boto3_assist/dynamodb/dynamodb_model_base_interfaces.py +0 -0
  76. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/src/boto3_assist/dynamodb/dynamodb_reindexer.py +0 -0
  77. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/src/boto3_assist/dynamodb/readme.md +0 -0
  78. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/src/boto3_assist/dynamodb/troubleshooting.md +0 -0
  79. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/src/boto3_assist/environment_services/__init__.py +0 -0
  80. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/src/boto3_assist/utilities/datetime_utility.py +0 -0
  81. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/src/boto3_assist/utilities/logging_utility.py +0 -0
  82. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/src/boto3_assist/utilities/serialization_utility.py +0 -0
  83. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/src/boto3_assist/utilities/string_utility.py +0 -0
  84. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/tests/__init__.py +0 -0
  85. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/tests/__top/__init__.py +0 -0
  86. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/tests/dynamodb/__init__.py +0 -0
  87. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/tests/dynamodb/dynamodb_model_base_test.py +0 -0
  88. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/tests/dynamodb/dynamodb_model_projections_test.py +0 -0
  89. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/tests/dynamodb/dynamodb_reindex_test.py +0 -0
  90. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/tests/dynamodb/models/simple_model.py +0 -0
  91. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/tests/dynamodb/models/user_model.py +0 -0
  92. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/tests/examples_test/__init__.py +0 -0
  93. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/tests/examples_test/user_service_test.py +0 -0
  94. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/tests/utilities/__init__.py +0 -0
  95. {boto3_assist-0.1.14 → boto3_assist-0.2.0}/tests/utilities/serialization_utility_test.py +0 -0
@@ -0,0 +1,17 @@
1
+ # let's make sure we don't hit any actual aws resources
2
+ # when running moto mock tests
3
+ AWS_ACCESS_KEY_ID="testing"
4
+ AWS_SECRET_ACCESS_KEY="testing"
5
+ AWS_SECURITY_TOKEN="testing"
6
+ AWS_SESSION_TOKEN="testing"
7
+ AWS_DEFAULT_REGION="us-east-1"
8
+ AWS_REGION="us-east-1"
9
+ # So far it looks like I need a profile or I get an error like the one below
10
+ # "The config profile (<profile>) could not be found"
11
+ # "The config profile () could not be found" <- if using the default profile
12
+ # AWS_PROFILE="default"
13
+ # AWS_DEFAULT_PROFILE="default"
14
+ # I don't want to use default profile, so I'm using a mock profile
15
+ # However it must exist in ~/.aws/config or you'll get the same error above
16
+ AWS_PROFILE="moto-mock-tests"
17
+ AWS_DEFAULT_PROFILE="moto-mock-tests"
@@ -161,4 +161,5 @@ cython_debug/
161
161
  # option (not recommended) you can uncomment the following to ignore the entire idea folder.
162
162
  #.idea/
163
163
 
164
- .imports
164
+ .imports
165
+ .env.development
@@ -1,10 +1,8 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: boto3_assist
3
- Version: 0.1.14
3
+ Version: 0.2.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
- License-File: LICENSE-EXPLAINED.txt
7
- License-File: LICENSE.txt
8
6
  Classifier: License :: Other/Proprietary License
9
7
  Classifier: Operating System :: OS Independent
10
8
  Classifier: Programming Language :: Python :: 3
@@ -0,0 +1,62 @@
1
+ import os
2
+ import json
3
+ import csv
4
+ from typing import List, Dict, Any
5
+ from pathlib import Path
6
+ from boto3_assist.environment_services.environment_loader import EnvironmentLoader
7
+ from boto3_assist.ec2.ec2_connection import EC2Connection
8
+ from boto3_assist.cloudwatch.cloudwatch_query import CloudWatchQuery
9
+
10
+
11
+ class LogReport(CloudWatchQuery):
12
+ def __init__(self) -> None:
13
+ super().__init__()
14
+
15
+ def export(self, regions: List[Dict[str, Any]], file_format: str = "csv"):
16
+ """
17
+ Export the regions to a file
18
+ Args:
19
+ regions (List[Dict[str, Any]]): _description_
20
+ file_format (str, optional): _description_. Defaults to "csv".
21
+ """
22
+ if file_format == "csv":
23
+ self.__export_regions_to_csv(regions)
24
+ elif file_format == "json":
25
+ self.__export_regions_to_json(regions)
26
+
27
+ def __export_regions_to_csv(self, regions, filename="aws_regions_with_status.csv"):
28
+ with open(filename, "w", newline="", encoding="utf-8") as csvfile:
29
+ writer = csv.writer(csvfile)
30
+ writer.writerow(["RegionName", "OptInStatus"])
31
+ for region in regions:
32
+ writer.writerow([region["RegionName"], region["OptInStatus"]])
33
+
34
+ def __export_regions_to_json(
35
+ self, regions, filename="aws_regions_with_status.json"
36
+ ):
37
+ with open(filename, "w", encoding="utf-8") as jsonfile:
38
+ json.dump(regions, jsonfile, indent=4)
39
+
40
+
41
+ def main():
42
+ """Main"""
43
+
44
+ env_file_name: str = os.getenv("ENVRIONMENT_FILE", ".env")
45
+ path = os.path.join(str(Path(__file__).parents[2].absolute()), env_file_name)
46
+ el: EnvironmentLoader = EnvironmentLoader()
47
+ if not os.path.exists(path=path):
48
+ raise FileNotFoundError("Failed to find the environmetn file")
49
+ loaded: bool = el.load_environment_file(path=path)
50
+ if not loaded:
51
+ raise RuntimeError("Failed to load my local environment")
52
+
53
+ report: CloudWatchQuery = CloudWatchQuery()
54
+
55
+ # Retrieve AWS regions with their status
56
+ logs = report.get_log_sizes()
57
+
58
+ print(logs)
59
+
60
+
61
+ if __name__ == "__main__":
62
+ main()
@@ -33,14 +33,18 @@ class OrderItem(DynamoDBModelBase):
33
33
  self.__setup_indexes()
34
34
 
35
35
  def __setup_indexes(self):
36
- # user id
36
+ # the primary key will be made off of the
37
+ # order.id and this item.id
38
+ # this will allow for a 1 to many search on the items related to an order
37
39
  primay: DynamoDBIndex = DynamoDBIndex()
38
40
  primay.name = "primary"
41
+ # create a partition key off of the order key
39
42
  primay.partition_key.attribute_name = "pk"
40
43
  primay.partition_key.value = lambda: DynamoDBKey.build_key(
41
44
  ("order", self.order_id)
42
45
  )
43
46
 
47
+ # create the sort key off of this items id
44
48
  primay.sort_key.attribute_name = "sk"
45
49
  primay.sort_key.value = lambda: DynamoDBKey.build_key(("item", self.id))
46
50
  self.indexes.add_primary(primay)
@@ -5,17 +5,18 @@ DynamoDB Example
5
5
  import json
6
6
  import os
7
7
  import random
8
+ from datetime import UTC, datetime, timedelta
8
9
  from pathlib import Path
9
10
  from typing import List
10
- from datetime import datetime, timedelta, UTC
11
+
11
12
  from boto3_assist.dynamodb.dynamodb import DynamoDB
12
13
  from boto3_assist.environment_services.environment_loader import EnvironmentLoader
13
14
  from boto3_assist.utilities.serialization_utility import JsonEncoder
14
15
  from boto3_assist.utilities.string_utility import StringUtility
15
- from examples.dynamodb.services.table_service import DynamoDBTableService
16
- from examples.dynamodb.services.order_service import OrderService, Order
17
- from examples.dynamodb.services.order_item_service import OrderItemService, OrderItem
18
16
  from examples.dynamodb.models.product_model import Product
17
+ from examples.dynamodb.services.order_item_service import OrderItem, OrderItemService
18
+ from examples.dynamodb.services.order_service import Order, OrderService
19
+ from examples.dynamodb.services.table_service import DynamoDBTableService
19
20
 
20
21
 
21
22
  class DynamoDBExample:
@@ -154,7 +155,7 @@ def main():
154
155
  el: EnvironmentLoader = EnvironmentLoader()
155
156
  if not os.path.exists(path=path):
156
157
  raise FileNotFoundError("Failed to find the environmetn file")
157
- loaded: bool = el.load_environment_file(path)
158
+ loaded: bool = el.load_environment_file(path=path)
158
159
  if not loaded:
159
160
  raise RuntimeError("Failed to load my local environment")
160
161
 
@@ -40,7 +40,7 @@ class OrderItemService:
40
40
 
41
41
  def list(self, order_id: str) -> list:
42
42
  """
43
- Lists users using a global secondary index.
43
+ Lists using a global secondary index.
44
44
 
45
45
  Args:
46
46
  user_id (str): Gets orders by a user id.
@@ -181,7 +181,7 @@ def main():
181
181
  el: EnvironmentLoader = EnvironmentLoader()
182
182
  if not os.path.exists(path=path):
183
183
  raise FileNotFoundError("Failed to find the environmetn file")
184
- loaded: bool = el.load_environment_file(path)
184
+ loaded: bool = el.load_environment_file(path=path)
185
185
  if not loaded:
186
186
  raise RuntimeError("Failed to load my local environment")
187
187
 
@@ -60,7 +60,7 @@ def main():
60
60
  el: EnvironmentLoader = EnvironmentLoader()
61
61
  if not os.path.exists(path=path):
62
62
  raise FileNotFoundError("Failed to find the environmetn file")
63
- loaded: bool = el.load_environment_file(path)
63
+ loaded: bool = el.load_environment_file(path=path)
64
64
  if not loaded:
65
65
  raise RuntimeError("Failed to load my local environment")
66
66
 
@@ -7,7 +7,7 @@ packages = ["src/boto3_assist"]
7
7
 
8
8
  [project]
9
9
  name = "boto3_assist"
10
- version = "0.1.14"
10
+ version = "0.2.0"
11
11
  authors = [
12
12
  { name="Eric Wilson", email="boto3-assist@geekcafe.com" }
13
13
  ]
@@ -3,6 +3,7 @@ build
3
3
  mypy
4
4
  mypy_boto3_dynamodb
5
5
  mypy_boto3_ec2
6
+ mypy_boto3_cloudwatch
6
7
  moto [dynamodb2] # mocks for unit tests
7
8
  # setuptools
8
9
  types-python-dateutil
@@ -45,6 +45,7 @@ class Boto3SessionManager:
45
45
  self.aws_access_key_id = aws_access_key_id
46
46
  self.aws_secret_access_key = aws_secret_access_key
47
47
  self.aws_session_token = aws_session_token
48
+
48
49
  self.__session: Any = None
49
50
  self.__client: Any = None
50
51
  self.__resource: Any = None
@@ -90,7 +91,7 @@ class Boto3SessionManager:
90
91
 
91
92
  def __get_aws_session(
92
93
  self, aws_profile: Optional[str] = None, aws_region: Optional[str] = None
93
- ) -> boto3.Session:
94
+ ) -> boto3.Session | None:
94
95
  """Get a boto3 session for AWS."""
95
96
  logger.debug({"profile": aws_profile, "region": aws_region})
96
97
  try:
@@ -153,7 +154,7 @@ class Boto3SessionManager:
153
154
  )
154
155
  return self.__resource
155
156
 
156
- def __create_boto3_session(self) -> boto3.Session:
157
+ def __create_boto3_session(self) -> boto3.Session | None:
157
158
  try:
158
159
  session = boto3.Session(
159
160
  profile_name=self.aws_profile,
@@ -0,0 +1,86 @@
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
8
+ from typing import TYPE_CHECKING
9
+
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
+ )
15
+ from boto3_assist.cloudwatch.cloudwatch_connection_tracker import (
16
+ CloudWatchConnectionTracker,
17
+ )
18
+ from boto3_assist.connection import Connection
19
+
20
+ if TYPE_CHECKING:
21
+ from mypy_boto3_cloudwatch import CloudWatchClient, CloudWatchServiceResource
22
+ else:
23
+ CloudWatchClient = object
24
+ CloudWatchServiceResource = object
25
+
26
+
27
+ logger = Logger()
28
+ tracker: CloudWatchConnectionTracker = CloudWatchConnectionTracker()
29
+
30
+
31
+ class CloudWatchConnection(Connection):
32
+ """CW Environment"""
33
+
34
+ def __init__(
35
+ self,
36
+ *,
37
+ aws_profile: Optional[str] = None,
38
+ aws_region: Optional[str] = None,
39
+ aws_access_key_id: Optional[str] = None,
40
+ aws_secret_access_key: Optional[str] = None,
41
+ ) -> None:
42
+ super().__init__(
43
+ service_name="cloudwatch",
44
+ aws_profile=aws_profile,
45
+ aws_region=aws_region,
46
+ aws_access_key_id=aws_access_key_id,
47
+ aws_secret_access_key=aws_secret_access_key,
48
+ )
49
+
50
+ self.__client: CloudWatchClient | None = None
51
+ self.__resource: CloudWatchServiceResource | None = None
52
+
53
+ self.raise_on_error: bool = True
54
+
55
+ @property
56
+ def client(self) -> CloudWatchClient:
57
+ """CloudWatch Client Connection"""
58
+ if self.__client is None:
59
+ logger.info("Creating CloudWatch Client")
60
+ self.__client = self.session.client
61
+
62
+ if self.raise_on_error and self.__client is None:
63
+ raise RuntimeError("CloudWatch Client is not available")
64
+ return self.__client
65
+
66
+ @client.setter
67
+ def client(self, value: CloudWatchClient):
68
+ logger.info("Setting CloudWatch Client")
69
+ self.__client = value
70
+
71
+ @property
72
+ def resource(self) -> CloudWatchServiceResource:
73
+ """CloudWatch Resource Connection"""
74
+ if self.__resource is None:
75
+ logger.info("Creating CloudWatch Resource")
76
+ self.__resource = self.session.resource
77
+
78
+ if self.raise_on_error and self.__resource is None:
79
+ raise RuntimeError("CloudWatch Resource is not available")
80
+
81
+ return self.__resource
82
+
83
+ @resource.setter
84
+ def resource(self, value: CloudWatchServiceResource):
85
+ logger.info("Setting CloudWatch Resource")
86
+ self.__resource = value
@@ -0,0 +1,17 @@
1
+ """
2
+ Geek Cafe, LLC
3
+ Maintainers: Eric Wilson
4
+ MIT License. See Project Root for the license information.
5
+ """
6
+
7
+ from boto3_assist.connection_tracker import ConnectionTracker
8
+
9
+
10
+ class CloudWatchConnectionTracker(ConnectionTracker):
11
+ """
12
+ Tracks CloudWatch Connection Requests.
13
+ Useful in for performance tuning and debugging.
14
+ """
15
+
16
+ def __init__(self) -> None:
17
+ super().__init__("CloudWatch")
@@ -0,0 +1,62 @@
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
8
+ from typing import TYPE_CHECKING
9
+
10
+ from aws_lambda_powertools import Logger
11
+
12
+ from boto3_assist.cloudwatch.cloudwatch_connection_tracker import (
13
+ CloudWatchConnectionTracker,
14
+ )
15
+ from boto3_assist.connection import Connection
16
+
17
+ if TYPE_CHECKING:
18
+ from mypy_boto3_logs import CloudWatchLogsClient
19
+ else:
20
+ CloudWatchLogsClient = object
21
+
22
+
23
+ logger = Logger()
24
+ tracker: CloudWatchConnectionTracker = CloudWatchConnectionTracker()
25
+
26
+
27
+ class CloudWatchConnection(Connection):
28
+ """CW Logs Environment"""
29
+
30
+ def __init__(
31
+ self,
32
+ *,
33
+ aws_profile: Optional[str] = None,
34
+ aws_region: Optional[str] = None,
35
+ aws_access_key_id: Optional[str] = None,
36
+ aws_secret_access_key: Optional[str] = None,
37
+ ) -> None:
38
+ super().__init__(
39
+ service_name="logs",
40
+ aws_profile=aws_profile,
41
+ aws_region=aws_region,
42
+ aws_access_key_id=aws_access_key_id,
43
+ aws_secret_access_key=aws_secret_access_key,
44
+ )
45
+
46
+ self.__client: CloudWatchLogsClient | None = None
47
+
48
+ @property
49
+ def client(self) -> CloudWatchLogsClient:
50
+ """CloudWatch Client Connection"""
51
+ if self.__client is None:
52
+ logger.debug("Creating CloudWatch Client")
53
+ self.__client = self.session.client
54
+
55
+ if self.raise_on_error and self.__client is None:
56
+ raise RuntimeError("CloudWatch Client is not available")
57
+ return self.__client
58
+
59
+ @client.setter
60
+ def client(self, value: CloudWatchLogsClient):
61
+ logger.debug("Setting CloudWatch Client")
62
+ self.__client = value
@@ -0,0 +1,39 @@
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, List, Dict, Any
8
+ from boto3_assist.cloudwatch.cloudwatch_log_connection import CloudWatchConnection
9
+
10
+
11
+ class CloudWatchLogs(CloudWatchConnection):
12
+ def __init__(
13
+ self,
14
+ *,
15
+ aws_profile: Optional[str] = None,
16
+ aws_region: Optional[str] = None,
17
+ aws_access_key_id: Optional[str] = None,
18
+ aws_secret_access_key: Optional[str] = None,
19
+ ) -> None:
20
+ super().__init__(
21
+ aws_profile=aws_profile,
22
+ aws_region=aws_region,
23
+ aws_access_key_id=aws_access_key_id,
24
+ aws_secret_access_key=aws_secret_access_key,
25
+ )
26
+
27
+ def list_log_groups(self):
28
+ """Retrieve all log groups in the AWS account."""
29
+ log_groups: List[Dict[str, Any]] = []
30
+ paginator = self.client.get_paginator("describe_log_groups")
31
+ for page in paginator.paginate():
32
+ log_groups.extend(page["logGroups"]) # type: ignore[arg-type]
33
+ return log_groups
34
+
35
+
36
+ def main():
37
+ query: CloudWatchLogs = CloudWatchLogs()
38
+ result = query.list_log_groups()
39
+ print(result)
@@ -0,0 +1,191 @@
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
+ from datetime import datetime, timedelta, UTC
9
+ from typing import Optional, Dict, Any, List
10
+ from boto3_assist.cloudwatch.cloudwatch_connection import CloudWatchConnection
11
+ from boto3_assist.cloudwatch.cloudwatch_logs import CloudWatchLogs
12
+
13
+
14
+ class CloudWatchQuery(CloudWatchConnection):
15
+ """Query Cloud Watch"""
16
+
17
+ def __init__(
18
+ self,
19
+ *,
20
+ aws_profile: Optional[str] = None,
21
+ aws_region: Optional[str] = None,
22
+ aws_access_key_id: Optional[str] = None,
23
+ aws_secret_access_key: Optional[str] = None,
24
+ ) -> None:
25
+ super().__init__(
26
+ aws_profile=aws_profile,
27
+ aws_region=aws_region,
28
+ aws_access_key_id=aws_access_key_id,
29
+ aws_secret_access_key=aws_secret_access_key,
30
+ )
31
+
32
+ self.__cw_logs: CloudWatchLogs | None = None
33
+
34
+ @property
35
+ def cw_logs(self) -> CloudWatchLogs:
36
+ """CloudWatch Logs Connection"""
37
+ if self.__cw_logs is None:
38
+ self.__cw_logs = CloudWatchLogs(
39
+ aws_profile=self.aws_profile,
40
+ aws_region=self.aws_region,
41
+ aws_access_key_id=self.aws_access_key_id,
42
+ aws_secret_access_key=self.aws_secret_access_key,
43
+ )
44
+ return self.__cw_logs
45
+
46
+ def get_log_group_size(
47
+ self, log_group_name: str, start_time: datetime, end_time: datetime
48
+ ) -> Dict[str, Any]:
49
+ """
50
+ Get the log group size for a given period of time
51
+ Args:
52
+ log_group_name (str): _description_
53
+ start_time (datetime): _description_
54
+ end_time (datetime): _description_
55
+
56
+ Returns:
57
+ _type_: _description_
58
+ """
59
+ response = self.client.get_metric_data(
60
+ MetricDataQueries=[
61
+ {
62
+ "Id": "storedBytes",
63
+ "MetricStat": {
64
+ "Metric": {
65
+ "Namespace": "AWS/Logs",
66
+ # "MetricName": "StoredBytes",
67
+ "MetricName": "IncomingBytes",
68
+ "Dimensions": [
69
+ {"Name": "LogGroupName", "Value": log_group_name}
70
+ ],
71
+ },
72
+ "Period": 86400, # Daily data
73
+ "Stat": "Sum",
74
+ },
75
+ "ReturnData": True,
76
+ },
77
+ ],
78
+ StartTime=start_time,
79
+ EndTime=end_time,
80
+ )
81
+
82
+ # Extract the total size in bytes for the period
83
+ size: float = 0.0
84
+ if response["MetricDataResults"]:
85
+ # Access the first MetricDataResult
86
+ metric_data_result = response["MetricDataResults"][0]
87
+ # Sum the values if they exist
88
+ size = (
89
+ sum(metric_data_result["Values"]) if metric_data_result["Values"] else 0
90
+ )
91
+ else:
92
+ size = 0
93
+
94
+ size_mb = size / (1024 * 1024)
95
+ size_gb = size_mb / 1024
96
+ resp: Dict[str, Any] = {
97
+ "LogGroupName": log_group_name,
98
+ "Size": {
99
+ "Bytes": size,
100
+ "MB": size_mb,
101
+ "GB": size_gb,
102
+ },
103
+ "StartDate": start_time.isoformat(),
104
+ "EndDate": end_time.isoformat(),
105
+ }
106
+
107
+ return resp
108
+
109
+ def get_log_sizes(
110
+ self,
111
+ start_date_time: datetime | None = None,
112
+ end_date_time: datetime | None = None,
113
+ days: int | None = 7,
114
+ top: int = 0,
115
+ ) -> List[Dict[str, Any]]:
116
+ """
117
+ Gets the log sizes for all log groups
118
+
119
+ Args:
120
+ start_date_time (datetime | None, optional): The Start Date. Defaults to None.
121
+ If None it's set to now in UTC time - the days field
122
+ end_date_time (datetime | None, optional): he Start Date. Defaults to None.
123
+ If None it's set to not in UTC time
124
+ days (int | None, optional): The days offset. Defaults to 7.
125
+ top (int, optional): If greater than zero it will return the top x after sorting
126
+ Defaults to 0.
127
+
128
+ Returns:
129
+ list: _description_
130
+ """
131
+ if not days:
132
+ days = 7
133
+ start_time = start_date_time or (datetime.now(UTC) - timedelta(days=days))
134
+ end_time = end_date_time or datetime.now(UTC)
135
+
136
+ # Step 1: List all log groups
137
+ log_groups = self.cw_logs.list_log_groups()
138
+ log_group_sizes = []
139
+
140
+ # Step 2: Get sizes for each log group
141
+ for log_group in log_groups:
142
+ log_group_name = log_group["logGroupName"]
143
+
144
+ size_info = self.get_log_group_size(log_group_name, start_time, end_time)
145
+ log_group_sizes.append(size_info)
146
+
147
+ # Step 3: Sort by size
148
+ # top_log_groups = sorted(log_group_sizes, key=lambda x: x[1], reverse=True)
149
+ top_log_groups = sorted(
150
+ log_group_sizes,
151
+ key=lambda x: x.get("Size", {}).get("Bytes", 0),
152
+ reverse=True,
153
+ )
154
+ if top and top > 0:
155
+ # find the top x if provided
156
+ top_log_groups = top_log_groups[:top]
157
+
158
+ return top_log_groups
159
+
160
+
161
+ def main():
162
+ log_group = os.environ.get("LOG_GROUP_QUERY_SAMPLE", "<enter-log-group-here>")
163
+ start = datetime.now() - timedelta(days=7) # Last 30 days
164
+ end = datetime.now()
165
+ cw_query: CloudWatchQuery = CloudWatchQuery()
166
+ result = cw_query.get_log_group_size(log_group, start, end)
167
+ print(result)
168
+
169
+ top = 25
170
+ days = 7
171
+ top_log_groups = cw_query.get_log_sizes(top=top, days=days)
172
+ print(f"Top {top} log groups by size for the last week:")
173
+
174
+ for top_log_group in top_log_groups:
175
+ log_group_name = top_log_group["LogGroupName"]
176
+ size_in_bytes = top_log_group.get("Size", {}).get("Bytes", 0)
177
+ size_in_megs = top_log_group.get("Size", {}).get("MB", 0)
178
+ size_in_gigs = top_log_group.get("Size", {}).get("GB", 0)
179
+ size: str = ""
180
+ if size_in_gigs > 1:
181
+ size = f"{size_in_gigs:.2f} GB"
182
+ elif size_in_megs > 1:
183
+ size = f"{size_in_megs:.2f} MB"
184
+ else:
185
+ size = f"{size_in_bytes} bytes"
186
+
187
+ print(f"{size}: {log_group_name}")
188
+
189
+
190
+ if __name__ == "__main__":
191
+ main()