boto3-assist 0.21.0__tar.gz → 0.23.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 (146) hide show
  1. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/.vscode/settings.json +3 -1
  2. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/PKG-INFO +1 -1
  3. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/pyproject.toml +1 -1
  4. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/dynamodb/dynamodb.py +49 -10
  5. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/dynamodb/dynamodb_index.py +10 -5
  6. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/dynamodb/dynamodb_model_base.py +9 -1
  7. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/dynamodb/dynamodb_model_base_interfaces.py +2 -2
  8. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/utilities/datetime_utility.py +8 -10
  9. boto3_assist-0.23.0/src/boto3_assist/version.py +1 -0
  10. boto3_assist-0.23.0/tests/unit/dynamodb_tests/db_models/task.py +76 -0
  11. boto3_assist-0.23.0/tests/unit/dynamodb_tests/dynamodb_fail_if_exists_test.py +59 -0
  12. boto3_assist-0.23.0/tests/unit/dynamodb_tests/dynamodb_primary_key_sort_test.py +82 -0
  13. boto3_assist-0.21.0/src/boto3_assist/version.py +0 -1
  14. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/.env.docker +0 -0
  15. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/.env.docker.001 +0 -0
  16. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/.env.docker.nosql.workbench +0 -0
  17. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/.env.unittest +0 -0
  18. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/.gitignore +0 -0
  19. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/.vscode/launch.json +0 -0
  20. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/.vscode/tasks.json +0 -0
  21. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/LICENSE-EXPLAINED.txt +0 -0
  22. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/LICENSE.txt +0 -0
  23. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/README.md +0 -0
  24. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/aws_regions_with_status.csv +0 -0
  25. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/aws_regions_with_status.json +0 -0
  26. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/devops/build.py +0 -0
  27. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/devops/readme.md +0 -0
  28. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/examples/__init__.py +0 -0
  29. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/examples/cloudwatch/log_report.py +0 -0
  30. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/examples/dynamodb/models/order_item_model.py +0 -0
  31. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/examples/dynamodb/models/order_model.py +0 -0
  32. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/examples/dynamodb/models/product_model.py +0 -0
  33. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/examples/dynamodb/models/user_model.py +0 -0
  34. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/examples/dynamodb/models/user_post_model.py +0 -0
  35. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/examples/dynamodb/order_example/main.py +0 -0
  36. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/examples/dynamodb/order_example/products.json +0 -0
  37. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/examples/dynamodb/services/order_item_service.py +0 -0
  38. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/examples/dynamodb/services/order_service.py +0 -0
  39. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/examples/dynamodb/services/product_service.py +0 -0
  40. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/examples/dynamodb/services/table_service.py +0 -0
  41. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/examples/dynamodb/services/user_post_service.py +0 -0
  42. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/examples/dynamodb/services/user_service.py +0 -0
  43. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/examples/dynamodb/services/user_service_client_example.py +0 -0
  44. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/examples/dynamodb/services/user_service_resource_example.py +0 -0
  45. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/examples/dynamodb/user_post_example/main.py +0 -0
  46. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/examples/ec2/regions_report.py +0 -0
  47. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/module-headers.txt +0 -0
  48. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/mypy.ini +0 -0
  49. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/requirements-dev.txt +0 -0
  50. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/requirements.txt +0 -0
  51. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/run-checks.sh +0 -0
  52. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/run_unit_tests.sh +0 -0
  53. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/__init__.py +0 -0
  54. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/aws_config.py +0 -0
  55. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/aws_lambda/event_info.py +0 -0
  56. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/aws_lambda/mock_context.py +0 -0
  57. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/boto3session.py +0 -0
  58. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/cloudwatch/cloudwatch_connection.py +0 -0
  59. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/cloudwatch/cloudwatch_connection_tracker.py +0 -0
  60. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/cloudwatch/cloudwatch_log_connection.py +0 -0
  61. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/cloudwatch/cloudwatch_logs.py +0 -0
  62. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/cloudwatch/cloudwatch_query.py +0 -0
  63. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/cognito/cognito_authorizer.py +0 -0
  64. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/cognito/cognito_connection.py +0 -0
  65. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/cognito/cognito_utility.py +0 -0
  66. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/cognito/jwks_cache.py +0 -0
  67. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/cognito/user.py +0 -0
  68. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/connection.py +0 -0
  69. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/connection_tracker.py +0 -0
  70. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/dynamodb/dynamodb_connection.py +0 -0
  71. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/dynamodb/dynamodb_helpers.py +0 -0
  72. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/dynamodb/dynamodb_importer.py +0 -0
  73. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/dynamodb/dynamodb_iservice.py +0 -0
  74. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/dynamodb/dynamodb_key.py +0 -0
  75. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/dynamodb/dynamodb_re_indexer.py +0 -0
  76. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/dynamodb/dynamodb_reindexer.py +0 -0
  77. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/dynamodb/dynamodb_reserved_words.py +0 -0
  78. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/dynamodb/dynamodb_reserved_words.txt +0 -0
  79. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/dynamodb/readme.md +0 -0
  80. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/dynamodb/troubleshooting.md +0 -0
  81. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/ec2/ec2_connection.py +0 -0
  82. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/environment_services/__init__.py +0 -0
  83. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/environment_services/environment_loader.py +0 -0
  84. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/environment_services/environment_variables.py +0 -0
  85. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/errors/custom_exceptions.py +0 -0
  86. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/http_status_codes.py +0 -0
  87. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/models/serializable_model.py +0 -0
  88. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/role_assumption_mixin.py +0 -0
  89. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/s3/s3.py +0 -0
  90. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/s3/s3_bucket.py +0 -0
  91. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/s3/s3_connection.py +0 -0
  92. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/s3/s3_event_data.py +0 -0
  93. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/s3/s3_object.py +0 -0
  94. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/securityhub/securityhub.py +0 -0
  95. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/securityhub/securityhub_connection.py +0 -0
  96. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/session_setup_mixin.py +0 -0
  97. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/ssm/connection.py +0 -0
  98. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/ssm/parameter_store/parameter_store.py +0 -0
  99. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/utilities/dictionary_utility.py +0 -0
  100. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/utilities/file_operations.py +0 -0
  101. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/utilities/http_utility.py +0 -0
  102. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/utilities/logging_utility.py +0 -0
  103. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/utilities/numbers_utility.py +0 -0
  104. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/utilities/serialization_utility.py +0 -0
  105. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/src/boto3_assist/utilities/string_utility.py +0 -0
  106. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/tests/__init__.py +0 -0
  107. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/tests/integration/cross_account_connection_test.py +0 -0
  108. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/tests/integration/tenant.py +0 -0
  109. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/tests/integration/tenant_services.py +0 -0
  110. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/tests/unit/aws_config_test.py +0 -0
  111. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/tests/unit/common/db_test_helpers.py +0 -0
  112. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/tests/unit/dynamodb_tests/__init__.py +0 -0
  113. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/tests/unit/dynamodb_tests/db_models/cms/base.py +0 -0
  114. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/tests/unit/dynamodb_tests/db_models/cms/content_block.py +0 -0
  115. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/tests/unit/dynamodb_tests/db_models/cms/page.py +0 -0
  116. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/tests/unit/dynamodb_tests/db_models/cms/template.py +0 -0
  117. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/tests/unit/dynamodb_tests/db_models/simple_model.py +0 -0
  118. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/tests/unit/dynamodb_tests/db_models/user_model.py +0 -0
  119. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/tests/unit/dynamodb_tests/db_models/user_required_fields_model.py +0 -0
  120. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/tests/unit/dynamodb_tests/dynamodb_model_base_test.py +0 -0
  121. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/tests/unit/dynamodb_tests/dynamodb_model_projections_test.py +0 -0
  122. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/tests/unit/dynamodb_tests/dynamodb_model_serializtion_test.py +0 -0
  123. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/tests/unit/dynamodb_tests/dynamodb_moto_sorting_test.py +0 -0
  124. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/tests/unit/dynamodb_tests/dynamodb_query_test.py +0 -0
  125. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/tests/unit/dynamodb_tests/dynamodb_reindex_test.py +0 -0
  126. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/tests/unit/examples_test/__init__.py +0 -0
  127. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/tests/unit/examples_test/user_service_test.py +0 -0
  128. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/tests/unit/lambda_tests/__init__.py +0 -0
  129. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/tests/unit/lambda_tests/event_info_test.py +0 -0
  130. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/tests/unit/models_tests/__init__.py +0 -0
  131. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/tests/unit/models_tests/models/person.py +0 -0
  132. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/tests/unit/models_tests/models/user.py +0 -0
  133. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/tests/unit/models_tests/serializable_model_person_test.py +0 -0
  134. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/tests/unit/models_tests/serializable_model_user_test.py +0 -0
  135. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/tests/unit/models_tests/serializable_model_wide_test.py +0 -0
  136. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/tests/unit/parameter_store/__init__.py +0 -0
  137. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/tests/unit/parameter_store/parameter_store_test.py +0 -0
  138. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/tests/unit/s3/__init__.py +0 -0
  139. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/tests/unit/s3/files/test.txt +0 -0
  140. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/tests/unit/s3/s3_event_data_test.py +0 -0
  141. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/tests/unit/s3/s3_file_delete_test.py +0 -0
  142. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/tests/unit/s3/s3_file_upload_test.py +0 -0
  143. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/tests/unit/session_tests/test_boto3_session_manager.py +0 -0
  144. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/tests/unit/utilities/__init__.py +0 -0
  145. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/tests/unit/utilities/serialization_utility_test.py +0 -0
  146. {boto3_assist-0.21.0 → boto3_assist-0.23.0}/tests/unit/utilities/string_utility_test.py +0 -0
@@ -28,7 +28,9 @@
28
28
  },
29
29
  "cSpell.words": [
30
30
  "addopts",
31
- "geekcafe"
31
+ "dateutil",
32
+ "geekcafe",
33
+ "relativedelta"
32
34
  ],
33
35
 
34
36
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boto3_assist
3
- Version: 0.21.0
3
+ Version: 0.23.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
@@ -15,7 +15,7 @@ addopts = "-m 'not integration'"
15
15
 
16
16
  [project]
17
17
  name = "boto3_assist"
18
- version = "0.21.0"
18
+ version = "0.23.0"
19
19
 
20
20
  authors = [
21
21
  { name="Eric Wilson", email="boto3-assist@geekcafe.com" }
@@ -6,6 +6,8 @@ MIT License. See Project Root for the license information.
6
6
 
7
7
  import os
8
8
  from typing import List, Optional, overload, Dict, Any
9
+ from botocore.exceptions import ClientError
10
+ from boto3.dynamodb.conditions import Attr
9
11
 
10
12
  from aws_lambda_powertools import Logger
11
13
  from boto3.dynamodb.conditions import (
@@ -65,17 +67,22 @@ class DynamoDB(DynamoDBConnection):
65
67
  item: dict | DynamoDBModelBase,
66
68
  table_name: str,
67
69
  source: Optional[str] = None,
70
+ fail_if_exists: bool = False,
68
71
  ) -> dict:
69
72
  """
70
73
  Save an item to the database
71
74
  Args:
72
- item (dict): DynamoDB Dictionary Object or DynamoDBModelBase. Supports the "client" or
73
- "resource" syntax
75
+ item (dict): DynamoDB Dictionary Object or DynamoDBModelBase.
76
+ Supports the "client" or "resource" syntax
74
77
  table_name (str): The DynamoDb Table Name
75
78
  source (str, optional): The source of the call, used for logging. Defaults to None.
79
+ fail_if_exists (bool, optional): Only allow it to insert once.
80
+ Fail if it already exits. This is useful for loggers, historical records,
81
+ tasks, etc. that should only be created once
76
82
 
77
83
  Raises:
78
- e: Any Error Raised
84
+ ClientError: Client specific errors
85
+ Exception: Any Error Raised
79
86
 
80
87
  Returns:
81
88
  dict: The Response from DynamoDB's put_item actions.
@@ -85,7 +92,7 @@ class DynamoDB(DynamoDBConnection):
85
92
 
86
93
  try:
87
94
  if not isinstance(item, dict):
88
- # attemp to convert it
95
+ # attempt to convert it
89
96
  if not isinstance(item, DynamoDBModelBase):
90
97
  raise RuntimeError(
91
98
  f"Item is not a dictionary or DynamoDBModelBase. Type: {type(item).__name__}. "
@@ -106,19 +113,49 @@ class DynamoDB(DynamoDBConnection):
106
113
 
107
114
  if isinstance(item, dict) and isinstance(next(iter(item.values())), dict):
108
115
  # Use boto3.client syntax
109
- response = dict(
110
- self.dynamodb_client.put_item(TableName=table_name, Item=item)
111
- )
116
+ # client API style
117
+ params = {
118
+ "TableName": table_name,
119
+ "Item": item,
120
+ }
121
+ if fail_if_exists:
122
+ # only insert if the item does *not* already exist
123
+ params["ConditionExpression"] = (
124
+ "attribute_not_exists(#pk) AND attribute_not_exists(#sk)"
125
+ )
126
+ params["ExpressionAttributeNames"] = {"#pk": "pk", "#sk": "sk"}
127
+ response = dict(self.dynamodb_client.put_item(**params))
128
+
112
129
  else:
113
130
  # Use boto3.resource syntax
114
131
  table = self.dynamodb_resource.Table(table_name)
115
- response = dict(table.put_item(Item=item)) # type: ignore[arg-type]
132
+ if fail_if_exists:
133
+ cond = Attr("pk").not_exists() & Attr("sk").not_exists()
134
+ response = dict(table.put_item(Item=item, ConditionExpression=cond))
135
+ else:
136
+ response = dict(table.put_item(Item=item))
137
+ # response = dict(table.put_item(Item=item)) # type: ignore[arg-type]
138
+
139
+ except ClientError as e:
140
+ if (
141
+ fail_if_exists
142
+ and e.response["Error"]["Code"] == "ConditionalCheckFailedException"
143
+ ):
144
+ raise RuntimeError(
145
+ f"Item with pk={item['pk']} already exists in {table_name} "
146
+ f"and fail_if_exists was set to {fail_if_exists}"
147
+ ) from e
148
+
149
+ logger.exception(
150
+ {"source": f"{source}", "metric_filter": "put_item", "error": str(e)}
151
+ )
152
+ raise
116
153
 
117
154
  except Exception as e: # pylint: disable=w0718
118
155
  logger.exception(
119
156
  {"source": f"{source}", "metric_filter": "put_item", "error": str(e)}
120
157
  )
121
- raise e
158
+ raise
122
159
 
123
160
  return response
124
161
 
@@ -318,7 +355,9 @@ class DynamoDB(DynamoDBConnection):
318
355
  key = key.key()
319
356
 
320
357
  kwargs: dict = {}
321
- if index_name:
358
+
359
+ if index_name and index_name != "primary":
360
+ # only include the index_name if we are not using our "primary" pk/sk
322
361
  kwargs["IndexName"] = f"{index_name}"
323
362
  kwargs["TableName"] = f"{table_name}"
324
363
  kwargs["KeyConditionExpression"] = key
@@ -37,7 +37,7 @@ class DynamoDBIndexes:
37
37
  if index.name in self.__indexes:
38
38
  raise ValueError(
39
39
  f"The index {index.name} is already defined in your model somewhere. "
40
- "This error is generated to protect you from unforseen issues. "
40
+ "This error is generated to protect you from unforeseen issues. "
41
41
  "If you models are inheriting from other models, you may have the primary defined twice."
42
42
  )
43
43
 
@@ -64,7 +64,7 @@ class DynamoDBIndexes:
64
64
  for _, v in self.__indexes.items():
65
65
  if v.partition_key.attribute_name == index.partition_key.attribute_name:
66
66
  raise ValueError(
67
- f"The attrubute {index.partition_key.attribute_name} is already being used by index "
67
+ f"The attribute {index.partition_key.attribute_name} is already being used by index "
68
68
  f"{v.name}. "
69
69
  f"Reusing this attribute would over write the value on index {v.name}"
70
70
  )
@@ -73,7 +73,7 @@ class DynamoDBIndexes:
73
73
  for _, v in self.__indexes.items():
74
74
  if v.sort_key.attribute_name == index.sort_key.attribute_name:
75
75
  raise ValueError(
76
- f"The attrubute {index.sort_key.attribute_name} is already being used by index "
76
+ f"The attribute {index.sort_key.attribute_name} is already being used by index "
77
77
  f"{v.name}. "
78
78
  f"Reusing this attribute would over write the value on index {v.name}"
79
79
  )
@@ -160,9 +160,14 @@ class DynamoDBIndex:
160
160
  ) -> dict | Key | ConditionBase | ComparisonCondition | Equals:
161
161
  """Get the key for a given index"""
162
162
  key: dict | Key | ConditionBase | ComparisonCondition | Equals
163
- if self.name == DynamoDBIndexes.PRIMARY_INDEX and include_sort_key:
163
+ if (
164
+ self.name == DynamoDBIndexes.PRIMARY_INDEX
165
+ and include_sort_key
166
+ # if it ends with a # we are assuming that we are doing a wild card mapping
167
+ and not str(self.sort_key.value).endswith("#")
168
+ ):
164
169
  # this is a direct primary key which is used in a get call
165
- # this is differenet than query keys
170
+ # this is different than query keys
166
171
  key = {}
167
172
  key[self.partition_key.attribute_name] = self.partition_key.value
168
173
 
@@ -21,6 +21,7 @@ from boto3_assist.dynamodb.dynamodb_index import (
21
21
  from boto3_assist.dynamodb.dynamodb_reserved_words import DynamoDBReservedWords
22
22
  from boto3_assist.utilities.datetime_utility import DatetimeUtility
23
23
  from boto3_assist.models.serializable_model import SerializableModel
24
+ from boto3_assist.utilities.string_utility import StringUtility
24
25
 
25
26
 
26
27
  def exclude_from_serialization(method):
@@ -190,7 +191,7 @@ class DynamoDBModelBase(SerializableModel):
190
191
  def to_dictionary(self, include_none: bool = True):
191
192
  """
192
193
  Convert the instance to a dictionary without an indexes/keys.
193
- Usefull for turning an object into a dictionary for serialization.
194
+ Useful for turning an object into a dictionary for serialization.
194
195
  This is the same as to_resource_dictionary(include_indexes=False)
195
196
  """
196
197
  return DynamoDBSerializer.to_resource_dictionary(
@@ -205,6 +206,13 @@ class DynamoDBModelBase(SerializableModel):
205
206
 
206
207
  return self.indexes.get(index_name)
207
208
 
209
+ @staticmethod
210
+ def generate_uuid(sortable: bool = True) -> str:
211
+ if sortable:
212
+ return StringUtility.generate_sortable_uuid()
213
+
214
+ return StringUtility.generate_uuid()
215
+
208
216
  @property
209
217
  @exclude_from_serialization
210
218
  def helpers(self) -> DynamoDBHelpers:
@@ -25,10 +25,10 @@ class HasKeys(Protocol):
25
25
  """Interface for classes that have primary and sort keys"""
26
26
 
27
27
  def get_pk(self, index_name: str) -> Optional[str]:
28
- """Inteface to get_pk"""
28
+ """Interface to get_pk"""
29
29
 
30
30
  def get_sk(self, index_name: str) -> Optional[str]:
31
- """Inteface to get_sk"""
31
+ """Interface to get_sk"""
32
32
 
33
33
  def get_key(self, index_name: str) -> And | Equals:
34
34
  """Get the index name and key"""
@@ -12,7 +12,6 @@ from aws_lambda_powertools import Logger
12
12
  from dateutil.relativedelta import relativedelta
13
13
 
14
14
 
15
-
16
15
  logger = Logger()
17
16
 
18
17
  _last_timestamp = None
@@ -43,11 +42,10 @@ class DatetimeUtility:
43
42
  @staticmethod
44
43
  def get_utc_now() -> datetime:
45
44
  # datetime.utcnow()
46
- # below is the prefered over datetime.utcnow()
45
+ # below is the preferred over datetime.utcnow()
47
46
  return datetime.now(timezone.utc)
48
47
 
49
48
  @staticmethod
50
-
51
49
  def string_to_date(string_date: str | datetime) -> datetime | None:
52
50
  """
53
51
  Description: takes a string value and returns it as a datetime.
@@ -269,7 +267,7 @@ class DatetimeUtility:
269
267
  months (int): the number of months
270
268
 
271
269
  Returns:
272
- datetime: One Month added to the input dt
270
+ datetime: X Month(s) added to the input dt
273
271
  """
274
272
  new_date = dt + relativedelta(months=+months)
275
273
  new_date = new_date + relativedelta(microseconds=-1)
@@ -278,14 +276,14 @@ class DatetimeUtility:
278
276
 
279
277
  @staticmethod
280
278
  def add_days(dt: datetime, days: int = 1) -> datetime:
281
- """Add a month to the current date
279
+ """Add a day to the current date
282
280
 
283
281
  Args:
284
282
  dt (datetime): datetime
285
- months (int): the number of months
283
+ days (int): the number of days, use a negative number to subtract
286
284
 
287
285
  Returns:
288
- datetime: One Month added to the input dt
286
+ datetime: X days added to the input dt
289
287
  """
290
288
  new_date = dt + relativedelta(days=+days)
291
289
  new_date = new_date + relativedelta(microseconds=-1)
@@ -314,7 +312,7 @@ class DatetimeUtility:
314
312
 
315
313
  Args:
316
314
  utc_datetime (datetime): datetime in utc
317
- timezone (str): 'US/Eastern', 'US/Moutain', etc
315
+ timezone (str): 'US/Eastern', 'US/Mountain', etc
318
316
 
319
317
  Returns:
320
318
  datetime: in the correct timezone
@@ -326,7 +324,7 @@ class DatetimeUtility:
326
324
 
327
325
  @staticmethod
328
326
  def get_timestamp(value: datetime | None | str) -> float:
329
- """Get a timestampe from a date or 0.0"""
327
+ """Get a timestamp from a date or 0.0"""
330
328
  if value is None:
331
329
  return 0.0
332
330
  if not isinstance(value, datetime):
@@ -339,7 +337,7 @@ class DatetimeUtility:
339
337
 
340
338
  @staticmethod
341
339
  def get_timestamp_or_none(value: datetime | None | str) -> float | None:
342
- """Get a timestampe from a date or None"""
340
+ """Get a timestamp from a date or None"""
343
341
  if value is None:
344
342
  return None
345
343
  if not isinstance(value, datetime):
@@ -0,0 +1 @@
1
+ __version__ = '0.23.0'
@@ -0,0 +1,76 @@
1
+ from typing import Optional
2
+ from boto3_assist.dynamodb.dynamodb_model_base import DynamoDBModelBase
3
+ from boto3_assist.dynamodb.dynamodb_index import DynamoDBIndex
4
+ from boto3_assist.dynamodb.dynamodb_key import DynamoDBKey
5
+ from boto3_assist.utilities.string_utility import StringUtility
6
+
7
+
8
+ class Task(DynamoDBModelBase):
9
+ """
10
+ A Generic Task Model
11
+
12
+ Using the same task.id, you can chain child elements via task.step_id
13
+
14
+ """
15
+
16
+ def __init__(self, id: Optional[str] = None):
17
+ super().__init__()
18
+ self.id: str = id or StringUtility.generate_uuid()
19
+ self._name: Optional[str] = None
20
+ self._step: Optional[str] = None
21
+ self._step_id: Optional[str] = None
22
+ self.metadata: dict = {}
23
+ self._setup_pk()
24
+
25
+ @property
26
+ def name(self) -> Optional[str]:
27
+ """
28
+ Returns the name for this task
29
+ """
30
+ return self._name
31
+
32
+ @name.setter
33
+ def name(self, value: str):
34
+ """
35
+ Sets the name for this task
36
+ """
37
+ self._name = value
38
+
39
+ @property
40
+ def step(self) -> Optional[str]:
41
+ """
42
+ Returns the step for this task
43
+ """
44
+ return self._step
45
+
46
+ @step.setter
47
+ def step(self, value: str):
48
+ """
49
+ Sets the step for this task
50
+ """
51
+ self._step = value
52
+
53
+ @property
54
+ def step_id(self) -> str | None:
55
+
56
+ return self._step_id
57
+
58
+ @step_id.setter
59
+ def step_id(self, value: str):
60
+ """
61
+ Sets the step_id for this task
62
+ """
63
+
64
+ self._step_id = value
65
+
66
+ def _setup_pk(self):
67
+ primary: DynamoDBIndex = DynamoDBIndex()
68
+ primary.name = "primary"
69
+ primary.partition_key.attribute_name = "pk"
70
+ primary.partition_key.value = lambda: DynamoDBKey.build_key(
71
+ (self.__class__.__name__, self.id)
72
+ )
73
+
74
+ primary.sort_key.attribute_name = "sk"
75
+ primary.sort_key.value = lambda: DynamoDBKey.build_key(("step", self.step_id))
76
+ self.indexes.add_primary(primary)
@@ -0,0 +1,59 @@
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
+ import moto
9
+ from typing import Optional
10
+ from tests.unit.dynamodb_tests.db_models.task import Task
11
+ from boto3_assist.environment_services.environment_loader import EnvironmentLoader
12
+ from boto3_assist.dynamodb.dynamodb import DynamoDB
13
+ from tests.unit.common.db_test_helpers import DbTestHelper
14
+
15
+
16
+ @moto.mock_aws
17
+ class DbQueryTest(unittest.TestCase):
18
+ "Serialization Tests"
19
+
20
+ def __init__(self, methodName="runTest"):
21
+ super().__init__(methodName)
22
+
23
+ ev: EnvironmentLoader = EnvironmentLoader()
24
+ # NOTE: you need to make sure the the env file below exists or you will get an error
25
+ ev.load_environment_file(file_name=".env.unittest")
26
+ self.__table_name = "mock_test_table"
27
+
28
+ self.db: DynamoDB = DynamoDB()
29
+
30
+ def setUp(self):
31
+ # load our test environment file to make sure we override any default AWS Environment Vars setup
32
+ # we don't want to accidentally connect to live environments
33
+ # https://docs.getmoto.org/en/latest/docs/getting_started.html
34
+
35
+ self.db: DynamoDB = self.db or DynamoDB()
36
+ DbTestHelper().helper_create_mock_table(self.__table_name, self.db.client)
37
+ print("Setup Complete")
38
+
39
+ def test_fail_if_exists(self):
40
+
41
+ task_id: str = "123456789"
42
+
43
+ task = Task(task_id)
44
+ response = self.db.save(table_name=self.__table_name, item=task)
45
+
46
+ self.assertEqual(response["ResponseMetadata"]["HTTPStatusCode"], 200)
47
+
48
+ # this will fail, fail_if_exists is set to true
49
+ self.assertRaises(
50
+ Exception,
51
+ self.db.save,
52
+ table_name=self.__table_name,
53
+ item=task,
54
+ fail_if_exists=True,
55
+ )
56
+
57
+ # this does not
58
+ response = self.db.save(table_name=self.__table_name, item=task)
59
+ self.assertEqual(response["ResponseMetadata"]["HTTPStatusCode"], 200)
@@ -0,0 +1,82 @@
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
+ import moto
9
+ from typing import Optional
10
+ from tests.unit.dynamodb_tests.db_models.task import Task
11
+ from boto3_assist.environment_services.environment_loader import EnvironmentLoader
12
+ from boto3_assist.dynamodb.dynamodb import DynamoDB
13
+ from tests.unit.common.db_test_helpers import DbTestHelper
14
+
15
+
16
+ @moto.mock_aws
17
+ class DbQueryTest(unittest.TestCase):
18
+ "Serialization Tests"
19
+
20
+ def __init__(self, methodName="runTest"):
21
+ super().__init__(methodName)
22
+
23
+ ev: EnvironmentLoader = EnvironmentLoader()
24
+ # NOTE: you need to make sure the the env file below exists or you will get an error
25
+ ev.load_environment_file(file_name=".env.unittest")
26
+ self.__table_name = "mock_test_table"
27
+
28
+ self.db: DynamoDB = DynamoDB()
29
+
30
+ def setUp(self):
31
+ # load our test environment file to make sure we override any default AWS Environment Vars setup
32
+ # we don't want to accidentally connect to live environments
33
+ # https://docs.getmoto.org/en/latest/docs/getting_started.html
34
+
35
+ self.db: DynamoDB = self.db or DynamoDB()
36
+ DbTestHelper().helper_create_mock_table(self.__table_name, self.db.client)
37
+ print("Setup Complete")
38
+
39
+ def create_data_process_task(
40
+ self,
41
+ name: str,
42
+ step: str,
43
+ task_id: Optional[str] = None,
44
+ metadata: Optional[dict] = None,
45
+ ) -> Task:
46
+ task = Task()
47
+ if task_id:
48
+ task.id = task_id
49
+ task.step_id = Task.generate_uuid()
50
+ else:
51
+ task.step_id = task.id
52
+ task.name = name
53
+ task.step = step
54
+
55
+ task.metadata = metadata
56
+ response = self.db.save(
57
+ table_name=self.__table_name, item=task, fail_if_exists=True
58
+ )
59
+ assert response["ResponseMetadata"]["HTTPStatusCode"] == 200
60
+ return task
61
+
62
+ def test_primary_key_query_sort(self):
63
+ task: Task = self.create_data_process_task(
64
+ name="test", step="test", task_id=None
65
+ )
66
+
67
+ # no create a bunch of children
68
+ for i in range(0, 10):
69
+ self.create_data_process_task(
70
+ name="test",
71
+ step=f"child-{i}",
72
+ task_id=task.id,
73
+ metadata={"child": f"i"},
74
+ )
75
+
76
+ query_model: Task = Task()
77
+ query_model.id = task.id
78
+ key = query_model.get_key("primary")
79
+ query_response = self.db.query(table_name=self.__table_name, key=key)
80
+
81
+ # primary task and 10 children == 11
82
+ assert len(query_response["Items"]) == 11
@@ -1 +0,0 @@
1
- __version__ = '0.21.0'
File without changes
File without changes
File without changes
File without changes
File without changes