boto3-assist 0.28.0__tar.gz → 0.30.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.
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/.gitignore +2 -1
- boto3_assist-0.30.0/.windsurf/rules/cascade.yaml +11 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/PKG-INFO +1 -1
- boto3_assist-0.30.0/docs/defining-models.md +237 -0
- boto3_assist-0.30.0/docs/defining-services.md +150 -0
- boto3_assist-0.30.0/docs/design-patterns.md +2468 -0
- boto3_assist-0.30.0/docs/issues/BOTO3_ASSIST_BEFORE_AFTER.md +362 -0
- boto3_assist-0.30.0/docs/issues/BOTO3_ASSIST_DECIMAL_PATTERN.md +446 -0
- boto3_assist-0.30.0/docs/issues/BOTO3_ASSIST_IMPLEMENTATION_CHECKLIST.md +327 -0
- boto3_assist-0.30.0/docs/unit-test-patterns.md +683 -0
- boto3_assist-0.30.0/examples/dynamodb/decimal_conversion_example.py +285 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/examples/dynamodb/models/product_model.py +6 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/examples/dynamodb/order_example/products.json +15 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/pyproject.toml +1 -1
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/dynamodb/dynamodb.py +25 -5
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/dynamodb/dynamodb_helpers.py +11 -13
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/dynamodb/dynamodb_model_base.py +12 -4
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/dynamodb/dynamodb_re_indexer.py +1 -1
- boto3_assist-0.30.0/src/boto3_assist/utilities/decimal_conversion_utility.py +140 -0
- boto3_assist-0.30.0/src/boto3_assist/version.py +1 -0
- boto3_assist-0.30.0/tests/unit/dynamodb/decimal_backward_compatibility_test.py +611 -0
- boto3_assist-0.30.0/tests/unit/dynamodb/decimal_conversion_integration_test.py +286 -0
- boto3_assist-0.30.0/tests/unit/dynamodb_tests/dynamodb_query_test.py +135 -0
- boto3_assist-0.30.0/tests/unit/utilities/decimal_conversion_utility_test.py +268 -0
- boto3_assist-0.28.0/src/boto3_assist/version.py +0 -1
- boto3_assist-0.28.0/tests/unit/dynamodb_tests/dynamodb_query_test.py +0 -66
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/.env.docker +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/.env.docker.001 +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/.env.docker.nosql.workbench +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/.env.unittest +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/.vscode/launch.json +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/.vscode/settings.json +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/.vscode/tasks.json +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/LICENSE-EXPLAINED.txt +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/LICENSE.txt +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/README.md +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/aws_regions_with_status.csv +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/aws_regions_with_status.json +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/devops/build.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/devops/readme.md +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/examples/__init__.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/examples/cloudwatch/log_report.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/examples/dynamodb/models/order_item_model.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/examples/dynamodb/models/order_model.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/examples/dynamodb/models/user_model.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/examples/dynamodb/models/user_post_model.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/examples/dynamodb/order_example/main.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/examples/dynamodb/services/order_item_service.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/examples/dynamodb/services/order_service.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/examples/dynamodb/services/product_service.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/examples/dynamodb/services/table_service.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/examples/dynamodb/services/user_post_service.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/examples/dynamodb/services/user_service.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/examples/dynamodb/services/user_service_client_example.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/examples/dynamodb/services/user_service_resource_example.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/examples/dynamodb/user_post_example/main.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/examples/ec2/regions_report.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/module-headers.txt +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/mypy.ini +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/publish_to_pypi.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/publish_to_pypi.sh +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/pysetup.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/pysetup.sh +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/requirements-dev.txt +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/requirements.dev.txt +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/requirements.txt +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/run-checks.sh +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/run_unit_tests.sh +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/setup.sh +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/__init__.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/aws_config.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/aws_lambda/event_info.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/aws_lambda/mock_context.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/boto3session.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/cloudwatch/cloudwatch_connection.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/cloudwatch/cloudwatch_connection_tracker.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/cloudwatch/cloudwatch_log_connection.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/cloudwatch/cloudwatch_logs.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/cloudwatch/cloudwatch_query.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/cognito/cognito_authorizer.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/cognito/cognito_connection.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/cognito/cognito_utility.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/cognito/jwks_cache.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/cognito/user.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/connection.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/connection_tracker.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/dynamodb/dynamodb_connection.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/dynamodb/dynamodb_importer.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/dynamodb/dynamodb_index.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/dynamodb/dynamodb_iservice.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/dynamodb/dynamodb_key.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/dynamodb/dynamodb_model_base_interfaces.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/dynamodb/dynamodb_reindexer.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/dynamodb/dynamodb_reserved_words.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/dynamodb/dynamodb_reserved_words.txt +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/dynamodb/readme.md +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/dynamodb/troubleshooting.md +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/ec2/ec2_connection.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/environment_services/__init__.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/environment_services/environment_loader.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/environment_services/environment_variables.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/erc/__init__.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/erc/ecr_connection.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/errors/custom_exceptions.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/http_status_codes.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/models/serializable_model.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/role_assumption_mixin.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/s3/s3.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/s3/s3_bucket.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/s3/s3_connection.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/s3/s3_event_data.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/s3/s3_object.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/securityhub/securityhub.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/securityhub/securityhub_connection.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/session_setup_mixin.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/ssm/connection.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/ssm/parameter_store/parameter_store.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/utilities/datetime_utility.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/utilities/dictionary_utility.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/utilities/file_operations.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/utilities/http_utility.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/utilities/logging_utility.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/utilities/numbers_utility.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/utilities/serialization_utility.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/src/boto3_assist/utilities/string_utility.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/__init__.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/integration/cross_account_connection_test.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/integration/tenant.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/integration/tenant_services.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/aws_config_test.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/common/db_test_helpers.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/dynamodb_tests/__init__.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/dynamodb_tests/db_models/cms/base.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/dynamodb_tests/db_models/cms/content_block.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/dynamodb_tests/db_models/cms/page.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/dynamodb_tests/db_models/cms/template.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/dynamodb_tests/db_models/simple_model.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/dynamodb_tests/db_models/task.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/dynamodb_tests/db_models/user_model.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/dynamodb_tests/db_models/user_required_fields_model.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/dynamodb_tests/dynamodb_fail_if_exists_test.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/dynamodb_tests/dynamodb_model_base_test.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/dynamodb_tests/dynamodb_model_projections_test.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/dynamodb_tests/dynamodb_model_serializtion_test.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/dynamodb_tests/dynamodb_moto_sorting_test.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/dynamodb_tests/dynamodb_primary_key_get_test.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/dynamodb_tests/dynamodb_primary_key_sort_test.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/dynamodb_tests/dynamodb_reindex_test.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/examples_test/__init__.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/examples_test/user_service_test.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/lambda_tests/__init__.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/lambda_tests/event_info_test.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/models_tests/__init__.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/models_tests/models/person.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/models_tests/models/user.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/models_tests/serializable_model_person_test.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/models_tests/serializable_model_user_test.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/models_tests/serializable_model_wide_test.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/parameter_store/__init__.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/parameter_store/parameter_store_test.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/s3/__init__.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/s3/files/test.txt +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/s3/s3_event_data_test.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/s3/s3_file_delete_test.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/s3/s3_file_upload_test.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/session_tests/test_boto3_session_manager.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/utilities/__init__.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/utilities/serialization_utility_test.py +0 -0
- {boto3_assist-0.28.0 → boto3_assist-0.30.0}/tests/unit/utilities/string_utility_test.py +0 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- name: Defining Models
|
|
3
|
+
path: docs/defining-models.md
|
|
4
|
+
description: Model layer design patterns
|
|
5
|
+
- name: Design Patterns
|
|
6
|
+
path: docs/design-patterns.md
|
|
7
|
+
description: Design patterns for building scalable SaaS applications
|
|
8
|
+
- name: Designing Services
|
|
9
|
+
path: docs/designing-services.md
|
|
10
|
+
description: Service layer design patterns
|
|
11
|
+
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# Defining DynamoDB Models with boto3-assist
|
|
2
|
+
|
|
3
|
+
This guide explains how to create and define DynamoDB models using the `DynamoDBModelBase` class provided by `boto3-assist`. Following these patterns ensures consistency, scalability, and easy integration with the `boto3-assist` ecosystem.
|
|
4
|
+
|
|
5
|
+
## 1. Introduction to DynamoDBModelBase
|
|
6
|
+
|
|
7
|
+
The `DynamoDBModelBase` is the foundation for all DynamoDB models in this framework. It provides a rich set of features out of the box, including:
|
|
8
|
+
|
|
9
|
+
- **Automatic Serialization**: Convert model instances to and from DynamoDB-compatible dictionaries.
|
|
10
|
+
- **Index Management**: A structured way to define primary keys and global secondary indexes (GSIs).
|
|
11
|
+
- **Data Mapping**: Easily map raw DynamoDB response data to your model instances.
|
|
12
|
+
- **Helper Utilities**: Access to utility functions for common tasks like timestamp conversion and UUID generation.
|
|
13
|
+
|
|
14
|
+
## 2. Creating a Basic Model
|
|
15
|
+
|
|
16
|
+
All models should inherit from `DynamoDBModelBase`. Here is an example of a simple `Product` model:
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
import datetime
|
|
20
|
+
from typing import Optional
|
|
21
|
+
from boto3_assist.dynamodb.dynamodb_model_base import DynamoDBModelBase
|
|
22
|
+
from boto3_assist.dynamodb.dynamodb_index import DynamoDBIndex, DynamoDBKey
|
|
23
|
+
|
|
24
|
+
class Product(DynamoDBModelBase):
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
id: Optional[str] = None,
|
|
28
|
+
name: Optional[str] = None,
|
|
29
|
+
price: float = 0.0,
|
|
30
|
+
description: Optional[str] = None,
|
|
31
|
+
sku: Optional[str] = None,
|
|
32
|
+
):
|
|
33
|
+
super().__init__()
|
|
34
|
+
|
|
35
|
+
self.id: Optional[str] = id
|
|
36
|
+
self.name: Optional[str] = name
|
|
37
|
+
self.price: float = price
|
|
38
|
+
self.description: Optional[str] = description
|
|
39
|
+
self.sku: Optional[str] = sku
|
|
40
|
+
|
|
41
|
+
# Initialize the indexes
|
|
42
|
+
self._setup_indexes()
|
|
43
|
+
|
|
44
|
+
def _setup_indexes(self):
|
|
45
|
+
# Index definitions will go here
|
|
46
|
+
pass
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Key Principles**:
|
|
50
|
+
|
|
51
|
+
- **Inheritance**: Your model must inherit from `DynamoDBModelBase`.
|
|
52
|
+
- **Constructor**: Define your model's attributes in the `__init__` method. Call `super().__init__()` at the beginning.
|
|
53
|
+
- **Index Setup**: Call a private method (e.g., `_setup_indexes()`) at the end of the constructor to define your keys and indexes.
|
|
54
|
+
|
|
55
|
+
## 3. Setting Up Indexes
|
|
56
|
+
|
|
57
|
+
DynamoDB keys and indexes are defined within the `_setup_indexes` method using the `DynamoDBIndex` and `DynamoDBKey` classes.
|
|
58
|
+
|
|
59
|
+
### Primary Key
|
|
60
|
+
|
|
61
|
+
Every model must have a primary key. The primary key is defined as a `DynamoDBIndex` and added to the model's `indexes` collection.
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
def _setup_indexes(self):
|
|
65
|
+
primary = DynamoDBIndex()
|
|
66
|
+
primary.name = "primary"
|
|
67
|
+
primary.partition_key.attribute_name = "pk"
|
|
68
|
+
primary.partition_key.value = lambda: DynamoDBKey.build_key(("product", self.id))
|
|
69
|
+
primary.sort_key.attribute_name = "sk"
|
|
70
|
+
primary.sort_key.value = lambda: DynamoDBKey.build_key(("product", self.id))
|
|
71
|
+
self.indexes.add_primary(primary)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- `DynamoDBIndex()`: Creates a new index definition.
|
|
75
|
+
- `partition_key` and `sort_key`: Define the attributes and values for your keys.
|
|
76
|
+
- `attribute_name`: The name of the attribute in the DynamoDB table (e.g., `pk`, `sk`).
|
|
77
|
+
- `value`: A **lambda function** that dynamically generates the key value. Using a lambda is crucial for ensuring the key is generated with the most current attribute values.
|
|
78
|
+
- `DynamoDBKey.build_key()`: A helper to construct composite keys with a consistent separator.
|
|
79
|
+
- `self.indexes.add_primary()`: Registers the index as the primary key.
|
|
80
|
+
|
|
81
|
+
### Global Secondary Indexes (GSIs)
|
|
82
|
+
|
|
83
|
+
You can add GSIs to support additional query patterns. GSIs are also defined as `DynamoDBIndex` objects and added using `add_secondary()`.
|
|
84
|
+
|
|
85
|
+
Here’s how to add a GSI to query all products by name:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
# Inside _setup_indexes method
|
|
89
|
+
|
|
90
|
+
self.indexes.add_secondary(
|
|
91
|
+
DynamoDBIndex(
|
|
92
|
+
index_name="gsi0",
|
|
93
|
+
partition_key=DynamoDBKey(
|
|
94
|
+
attribute_name="gsi0_pk",
|
|
95
|
+
# Use a static value for the partition key to query all products
|
|
96
|
+
value=lambda: DynamoDBKey.build_key(("products", ""))
|
|
97
|
+
),
|
|
98
|
+
sort_key=DynamoDBKey(
|
|
99
|
+
attribute_name="gsi0_sk",
|
|
100
|
+
value=lambda: DynamoDBKey.build_key(("name", self.name))
|
|
101
|
+
),
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
- `index_name`: The name of the GSI in your DynamoDB table (e.g., `gsi0`).
|
|
107
|
+
- `add_secondary()`: Registers the index as a GSI.
|
|
108
|
+
|
|
109
|
+
### Advanced GSI Patterns
|
|
110
|
+
|
|
111
|
+
Your models can support more complex query patterns by combining static partition keys with simple or composite sort keys.
|
|
112
|
+
|
|
113
|
+
#### Querying All Items with a Composite Sort Key
|
|
114
|
+
|
|
115
|
+
This pattern is useful when you want to retrieve all items of a specific type and sort them by multiple fields. For example, to get all users sorted by `last_name` and then `first_name`.
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
# Inside _setup_indexes for a UserModel
|
|
119
|
+
|
|
120
|
+
# GSI to list all users, sorted by last name, then first name
|
|
121
|
+
gsi_by_lastname = DynamoDBIndex(
|
|
122
|
+
index_name="gsi2",
|
|
123
|
+
partition_key=DynamoDBKey(
|
|
124
|
+
attribute_name="gsi2_pk",
|
|
125
|
+
# Static partition key to group all users together
|
|
126
|
+
value=lambda: DynamoDBKey.build_key(("users", None))
|
|
127
|
+
),
|
|
128
|
+
sort_key=DynamoDBKey(
|
|
129
|
+
attribute_name="gsi2_sk",
|
|
130
|
+
# Composite sort key
|
|
131
|
+
value=lambda: DynamoDBKey.build_key(
|
|
132
|
+
("lastname", self.last_name), ("firstname", self.first_name)
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
self.indexes.add_secondary(gsi_by_lastname)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**How it works**:
|
|
140
|
+
- **Static Partition Key**: `DynamoDBKey.build_key(("users", None))` generates a static partition key (e.g., `"users"`). This forces all user items into the same item collection within the GSI, allowing you to query all of them at once.
|
|
141
|
+
- **Composite Sort Key**: `DynamoDBKey.build_key(("lastname", self.last_name), ("firstname", self.first_name))` creates a sort key like `lastname#Smith#firstname#John`. This enables lexicographical sorting, first by last name and then by first name.
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
### What the Generated Keys Look Like
|
|
145
|
+
|
|
146
|
+
It's helpful to visualize what these key definitions produce. Given a `Product` instance:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
product = Product(id='abc-123', name='Mjolnir')
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
When you serialize this model using `to_resource_dictionary()`, the `boto3-assist` framework will generate the following key attributes based on the `lambda` functions in your index setup:
|
|
153
|
+
|
|
154
|
+
- **`pk`**: `product#abc-123`
|
|
155
|
+
- **`sk`**: `product#abc-123`
|
|
156
|
+
- **`gsi0_pk`**: `products`
|
|
157
|
+
- **`gsi0_sk`**: `name#Mjolnir`
|
|
158
|
+
|
|
159
|
+
Your final item in DynamoDB would look something like this:
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
{
|
|
163
|
+
"pk": "product#abc-123",
|
|
164
|
+
"sk": "product#abc-123",
|
|
165
|
+
"gsi0_pk": "products",
|
|
166
|
+
"gsi0_sk": "name#Mjolnir",
|
|
167
|
+
"id": "abc-123",
|
|
168
|
+
"name": "Mjolnir",
|
|
169
|
+
"price": 0.0,
|
|
170
|
+
"description": null,
|
|
171
|
+
"sku": null
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
This structure allows you to:
|
|
176
|
+
- Fetch the product directly using its `pk` and `sk`.
|
|
177
|
+
- Query all products on `gsi0` (where `gsi0_pk` is `"products"`) and sort them by name (`gsi0_sk`).
|
|
178
|
+
|
|
179
|
+
## 4. Serialization and Deserialization
|
|
180
|
+
|
|
181
|
+
`DynamoDBModelBase` provides powerful methods for serialization (Python object to dictionary) and deserialization (dictionary to Python object).
|
|
182
|
+
|
|
183
|
+
### Deserialization with `map()`
|
|
184
|
+
|
|
185
|
+
The `map()` method is the primary way to populate a model instance from a dictionary. It intelligently handles various DynamoDB response formats.
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
# Raw DynamoDB item
|
|
189
|
+
dynamodb_item = {
|
|
190
|
+
'pk': {'S': 'product#123'},
|
|
191
|
+
'sk': {'S': 'product#123'},
|
|
192
|
+
'name': {'S': 'Mjolnir'},
|
|
193
|
+
'price': {'N': '9999.99'}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
# Create an empty model and map the data
|
|
197
|
+
product = Product().map(dynamodb_item)
|
|
198
|
+
|
|
199
|
+
print(product.name) # Output: Mjolnir
|
|
200
|
+
print(product.price) # Output: 9999.99
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
As noted in a previous session, the `map()` method can handle:
|
|
204
|
+
- Full DynamoDB responses: `{'Item': {...}, 'ResponseMetadata': {...}}`
|
|
205
|
+
- Item-only responses: `{'Item': {...}}`
|
|
206
|
+
- Plain dictionaries.
|
|
207
|
+
|
|
208
|
+
### Serialization
|
|
209
|
+
|
|
210
|
+
There are several methods to convert a model instance to a dictionary:
|
|
211
|
+
|
|
212
|
+
- `to_dictionary()`: Returns a dictionary of the model's attributes, excluding any index attributes. This is useful for general-purpose serialization.
|
|
213
|
+
- `to_resource_dictionary()`: Returns a dictionary suitable for the Boto3 DynamoDB **Resource** API, including index attributes.
|
|
214
|
+
- `to_client_dictionary()`: Returns a dictionary suitable for the Boto3 DynamoDB **Client** API, with values serialized into DynamoDB's type format (e.g., `{'S': 'value'}`).
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
product = Product(id='456', name='Stormbreaker', price=8500.0)
|
|
218
|
+
|
|
219
|
+
# Get a simple dictionary of attributes
|
|
220
|
+
plain_dict = product.to_dictionary()
|
|
221
|
+
# {'id': '456', 'name': 'Stormbreaker', 'price': 8500.0, ...}
|
|
222
|
+
|
|
223
|
+
# Get a dictionary for the DynamoDB Resource API
|
|
224
|
+
resource_dict = product.to_resource_dictionary()
|
|
225
|
+
# {'pk': 'product#456', 'sk': 'product#456', 'id': '456', ...}
|
|
226
|
+
|
|
227
|
+
# Get a dictionary for the DynamoDB Client API
|
|
228
|
+
client_dict = product.to_client_dictionary()
|
|
229
|
+
# {'pk': {'S': 'product#456'}, 'sk': {'S': 'product#456'}, ...}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## 5. Best Practices
|
|
233
|
+
|
|
234
|
+
- **Models as Data Transfer Objects (DTOs)**: Models should only contain data and serialization logic. Avoid adding business logic or direct database calls (e.g., a `save()` method). Keep that logic in your service layer.
|
|
235
|
+
- **Use Lambda for Keys**: Always use `lambda` functions for key values to ensure they are generated at the time of serialization.
|
|
236
|
+
- **Consistent Naming**: Follow consistent naming conventions for your indexes and attributes (e.g., `gsi0`, `gsi1_pk`, `gsi1_sk`).
|
|
237
|
+
- **Call `_setup_indexes` in `__init__`**: Ensure your indexes are defined every time a model is instantiated.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Defining Services for DynamoDB Operations
|
|
2
|
+
|
|
3
|
+
This guide explains how to create a service layer to handle business logic and interact with DynamoDB using `boto3-assist`. The service layer is responsible for all Create, Read, Update, Delete, and List (CRUDL) operations.
|
|
4
|
+
|
|
5
|
+
## 1. The Role of the Service Layer
|
|
6
|
+
|
|
7
|
+
The service layer acts as an intermediary between your application's handlers (e.g., API endpoints) and the database. Its primary responsibilities are:
|
|
8
|
+
|
|
9
|
+
- **Encapsulating Business Logic**: All logic related to data manipulation resides here.
|
|
10
|
+
- **Interacting with the Database**: Services are the only part of your application that should directly call the `DynamoDB` class.
|
|
11
|
+
- **Using Models**: Services use the `DynamoDBModelBase` models to pass data to and from the database.
|
|
12
|
+
- **Error Handling**: Managing database exceptions and returning consistent responses.
|
|
13
|
+
|
|
14
|
+
By centralizing database interactions in a service layer, you create a clear separation of concerns, making your application easier to maintain, test, and scale.
|
|
15
|
+
|
|
16
|
+
## 2. Creating a Basic Service
|
|
17
|
+
|
|
18
|
+
A service is a Python class that initializes an instance of the `boto3_assist.dynamodb.DynamoDB` class. It also needs the name of the DynamoDB table it will be interacting with, which is typically stored in an environment variable.
|
|
19
|
+
|
|
20
|
+
Here is a basic `ProductService`:
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
import os
|
|
24
|
+
from typing import Optional
|
|
25
|
+
from boto3_assist.dynamodb.dynamodb import DynamoDB
|
|
26
|
+
from ..models.product_model import Product
|
|
27
|
+
|
|
28
|
+
class ProductService:
|
|
29
|
+
def __init__(self, db: Optional[DynamoDB] = None):
|
|
30
|
+
self.db = db or DynamoDB()
|
|
31
|
+
self.table_name = os.environ.get("APP_TABLE_NAME", "products-table")
|
|
32
|
+
|
|
33
|
+
# CRUDL methods will go here
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
- **Initialization**: The constructor accepts an optional `DynamoDB` instance, which is useful for dependency injection during testing. If one isn't provided, it creates a new instance.
|
|
37
|
+
- **Table Name**: The service gets the table name from an environment variable, providing a sensible default.
|
|
38
|
+
|
|
39
|
+
## 3. Implementing CRUDL Operations
|
|
40
|
+
|
|
41
|
+
Here’s how to implement the standard CRUDL operations in your service.
|
|
42
|
+
|
|
43
|
+
### Create
|
|
44
|
+
|
|
45
|
+
The `create` method uses the `db.save()` method to persist a new item to the database. It takes a model instance, converts it to a dictionary, and saves it.
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
def create_product(self, product_data: dict) -> Product:
|
|
49
|
+
"""Creates a new product."""
|
|
50
|
+
product = Product().map(product_data)
|
|
51
|
+
|
|
52
|
+
# The to_resource_dictionary() method includes the pk, sk, and any GSI keys
|
|
53
|
+
item_to_save = product.to_resource_dictionary()
|
|
54
|
+
|
|
55
|
+
self.db.save(item=item_to_save, table_name=self.table_name)
|
|
56
|
+
|
|
57
|
+
return product
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Read (Get by ID)
|
|
61
|
+
|
|
62
|
+
The `get` method retrieves a single item by its primary key. You create a model instance, set its ID, and pass it to the `db.get()` method.
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
def get_product_by_id(self, product_id: str) -> Optional[Product]:
|
|
66
|
+
"""Retrieves a product by its ID."""
|
|
67
|
+
# Create a model with the ID to identify the key
|
|
68
|
+
model_to_find = Product(id=product_id)
|
|
69
|
+
|
|
70
|
+
response = self.db.get(model=model_to_find, table_name=self.table_name)
|
|
71
|
+
|
|
72
|
+
item = response.get("Item")
|
|
73
|
+
if not item:
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
return Product().map(item)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Update
|
|
80
|
+
|
|
81
|
+
Updates are typically handled using a "get-then-save" pattern. You first retrieve the existing item, map the updates to it, and then save it back to the database. This ensures you don't accidentally overwrite data.
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
def update_product(self, product_id: str, updates: dict) -> Optional[Product]:
|
|
85
|
+
"""Updates an existing product."""
|
|
86
|
+
# 1. Get the existing product
|
|
87
|
+
existing_product = self.get_product_by_id(product_id)
|
|
88
|
+
if not existing_product:
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
# 2. Map the updates to the model
|
|
92
|
+
existing_product.map(updates)
|
|
93
|
+
|
|
94
|
+
# 3. Save it back to the database
|
|
95
|
+
item_to_save = existing_product.to_resource_dictionary()
|
|
96
|
+
self.db.save(item=item_to_save, table_name=self.table_name)
|
|
97
|
+
|
|
98
|
+
return existing_product
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Delete
|
|
102
|
+
|
|
103
|
+
The `delete` method removes an item from the database using its primary key. Similar to the `get` method, you pass a model instance with the ID set.
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
def delete_product(self, product_id: str) -> bool:
|
|
107
|
+
"""Deletes a product by its ID."""
|
|
108
|
+
product_to_delete = Product(id=product_id)
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
self.db.delete(model=product_to_delete, table_name=self.table_name)
|
|
112
|
+
return True
|
|
113
|
+
except Exception as e:
|
|
114
|
+
# Log the error
|
|
115
|
+
print(f"Error deleting product {product_id}: {e}")
|
|
116
|
+
return False
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### List (Query)
|
|
120
|
+
|
|
121
|
+
To list items, you typically query a Global Secondary Index (GSI). The `db.query()` method is used for this. You need to provide the index name and a `KeyConditionExpression`.
|
|
122
|
+
|
|
123
|
+
Here’s how to list all products, sorted by name, using the `gsi0` we defined in the model documentation:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from boto3.dynamodb.conditions import Key
|
|
127
|
+
|
|
128
|
+
def list_all_products(self):
|
|
129
|
+
"""Lists all products, sorted by name."""
|
|
130
|
+
# This key queries for all items where the GSI partition key is 'products'.
|
|
131
|
+
# This is based on the GSI we defined in the Product model.
|
|
132
|
+
key_condition = Key('gsi0_pk').eq(Product().get_key('gsi0').partition_key.value())
|
|
133
|
+
|
|
134
|
+
response = self.db.query(
|
|
135
|
+
key=key_condition,
|
|
136
|
+
index_name="gsi0",
|
|
137
|
+
table_name=self.table_name,
|
|
138
|
+
ascending=True
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
items = response.get("Items", [])
|
|
142
|
+
return [Product().map(item) for item in items]
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## 4. Best Practices
|
|
146
|
+
|
|
147
|
+
- **Dependency Injection**: Always allow the `DynamoDB` instance to be injected into your service's constructor. This is critical for testing.
|
|
148
|
+
- **Environment Variables**: Load sensitive information like table names from environment variables, not hardcoded strings.
|
|
149
|
+
- **Use Models**: Leverage your `DynamoDBModelBase` models for all data interactions. This ensures your keys are generated correctly and your data is properly serialized.
|
|
150
|
+
- **Separation of Concerns**: Keep business logic inside the service. API handlers should only be responsible for parsing requests and formatting responses.
|