aws-python-helper 0.30.1__tar.gz → 0.31.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 (44) hide show
  1. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/PKG-INFO +139 -1
  2. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/README.md +138 -0
  3. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/__init__.py +4 -0
  4. aws_python_helper-0.31.0/aws_python_helper/repository/__init__.py +3 -0
  5. aws_python_helper-0.31.0/aws_python_helper/repository/base.py +188 -0
  6. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper.egg-info/PKG-INFO +139 -1
  7. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper.egg-info/SOURCES.txt +2 -0
  8. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/pyproject.toml +1 -1
  9. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/api/__init__.py +0 -0
  10. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/api/auth_middleware.py +0 -0
  11. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/api/auth_validators.py +0 -0
  12. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/api/base.py +0 -0
  13. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/api/dispatcher.py +0 -0
  14. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/api/exceptions.py +0 -0
  15. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/api/fetcher.py +0 -0
  16. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/api/handler.py +0 -0
  17. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/database/__init__.py +0 -0
  18. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/database/database_proxy.py +0 -0
  19. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/database/external_database_proxy.py +0 -0
  20. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/database/external_mongo_manager.py +0 -0
  21. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/database/mongo_manager.py +0 -0
  22. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/fargate/__init__.py +0 -0
  23. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/fargate/executor.py +0 -0
  24. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/fargate/fetcher.py +0 -0
  25. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/fargate/handler.py +0 -0
  26. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/fargate/task_base.py +0 -0
  27. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/lambda_standalone/__init__.py +0 -0
  28. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/lambda_standalone/base.py +0 -0
  29. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/lambda_standalone/fetcher.py +0 -0
  30. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/lambda_standalone/handler.py +0 -0
  31. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/sns/__init__.py +0 -0
  32. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/sns/publisher.py +0 -0
  33. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/sqs/__init__.py +0 -0
  34. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/sqs/consumer_base.py +0 -0
  35. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/sqs/fetcher.py +0 -0
  36. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/sqs/handler.py +0 -0
  37. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/utils/__init__.py +0 -0
  38. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/utils/json_encoder.py +0 -0
  39. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/utils/response.py +0 -0
  40. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/utils/serializer.py +0 -0
  41. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper.egg-info/dependency_links.txt +0 -0
  42. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper.egg-info/requires.txt +0 -0
  43. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper.egg-info/top_level.txt +0 -0
  44. {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aws-python-helper
3
- Version: 0.30.1
3
+ Version: 0.31.0
4
4
  Summary: AWS Python Helper Framework
5
5
  Author-email: Fabian Claros <neufabiae@gmail.com>
6
6
  License: MIT
@@ -60,6 +60,7 @@ All available classes and functions:
60
60
  | `FargateTask` | `aws_python_helper.fargate.task_base` | Base class for Fargate tasks |
61
61
  | `FargateExecutor` | `aws_python_helper.fargate.executor` | Launches Fargate tasks from Lambda |
62
62
  | `fargate_handler` | `aws_python_helper.fargate.handler` | Entry point handler for Fargate |
63
+ | `Repository` | `aws_python_helper.repository.base` | Base class for MongoDB repositories |
63
64
  | `MongoJSONEncoder` | `aws_python_helper.utils.json_encoder` | JSON encoder for MongoDB types |
64
65
  | `mongo_json_dumps` | `aws_python_helper.utils.json_encoder` | Helper to serialize MongoDB types |
65
66
  | `serialize_mongo_types` | `aws_python_helper.utils.serializer` | Recursively serialize MongoDB types |
@@ -489,6 +490,143 @@ class AddressAPI(API):
489
490
 
490
491
  `self.external_db` is available in `API`, `SQSConsumer`, `Lambda`, and `FargateTask`.
491
492
 
493
+ ## 🗂️ Repository Pattern
494
+
495
+ The framework provides a `Repository` base class that eliminates repetitive boilerplate in data access layers. Each repository only declares what collection it uses, whether it belongs to an external cluster, and what indexes to create. The base class handles the MongoDB connection and index creation automatically.
496
+
497
+ ### Properties to override
498
+
499
+ | Property | Type | Default | Required |
500
+ |----------|------|---------|----------|
501
+ | `collection_name` | `str` | — | **Yes** |
502
+ | `database_name` | `str` | `"core"` | No |
503
+ | `is_external` | `bool` | `False` | No |
504
+ | `cluster_name` | `str` | `None` | Only if `is_external=True` |
505
+ | `indexes` | `list` | `[]` | No |
506
+
507
+ ### Index format
508
+
509
+ ```python
510
+ @property
511
+ def indexes(self):
512
+ return [
513
+ {"key": [("field", 1)]}, # simple ASC
514
+ {"key": [("field", -1)]}, # simple DESC
515
+ {"key": [("f1", 1), ("f2", -1)], "unique": True}, # compound + unique
516
+ {"key": [("expires_at", 1)], "expireAfterSeconds": 0}, # TTL index
517
+ ]
518
+ ```
519
+
520
+ Indexes are created automatically in the background on first collection access — no need to call any initialization method.
521
+
522
+ ### Repository on the main cluster (`database_name` defaults to `"core"`)
523
+
524
+ ```python
525
+ from aws_python_helper import Repository
526
+
527
+ class TownsRepository(Repository):
528
+
529
+ @property
530
+ def collection_name(self):
531
+ return "towns"
532
+
533
+ @property
534
+ def indexes(self):
535
+ return [
536
+ {"key": [("name", 1)]},
537
+ {"key": [("platform", 1)]},
538
+ ]
539
+
540
+ async def get_available(self, platforms):
541
+ return await self.collection.find(
542
+ {"platform": {"$in": platforms}},
543
+ {"name": 1, "platform": 1}
544
+ ).to_list(length=None)
545
+
546
+ async def find_by_name(self, name):
547
+ return await self.collection.find_one({"name": name})
548
+ ```
549
+
550
+ ### Repository on a different database (not `"core"`)
551
+
552
+ ```python
553
+ from aws_python_helper import Repository
554
+
555
+ class LandRecordsRepository(Repository):
556
+
557
+ @property
558
+ def database_name(self):
559
+ return "land_data"
560
+
561
+ @property
562
+ def collection_name(self):
563
+ return "records"
564
+
565
+ @property
566
+ def indexes(self):
567
+ return [
568
+ {"key": [("unique_id", 1)]},
569
+ {"key": [("owner", 1), ("town", 1)]},
570
+ ]
571
+
572
+ async def bulk_upsert(self, records):
573
+ from pymongo import UpdateOne
574
+ operations = [
575
+ UpdateOne({"unique_id": r["unique_id"]}, {"$set": r}, upsert=True)
576
+ for r in records
577
+ ]
578
+ result = await self.collection.bulk_write(operations)
579
+ return {"upserted": result.upserted_count, "modified": result.modified_count}
580
+ ```
581
+
582
+ ### Repository on an external cluster
583
+
584
+ ```python
585
+ from aws_python_helper import Repository
586
+
587
+ class AddressRepository(Repository):
588
+
589
+ @property
590
+ def database_name(self):
591
+ return "smart_data"
592
+
593
+ @property
594
+ def collection_name(self):
595
+ return "address"
596
+
597
+ @property
598
+ def is_external(self):
599
+ return True
600
+
601
+ @property
602
+ def cluster_name(self):
603
+ return "ClusterDockets" # Must match a name in EXTERNAL_MONGODB_CONNECTIONS
604
+
605
+ async def find_by_query(self, query, limit=None):
606
+ cursor = self.collection.find(query)
607
+ if limit:
608
+ cursor = cursor.limit(limit)
609
+ return await cursor.to_list(length=None)
610
+ ```
611
+
612
+ ### Instantiation — no `db` argument needed
613
+
614
+ ```python
615
+ class MyAPI(API):
616
+
617
+ @property
618
+ def towns_repository(self):
619
+ if not self._towns_repository:
620
+ self._towns_repository = TownsRepository() # no args!
621
+ return self._towns_repository
622
+
623
+ async def process(self):
624
+ towns = await self.towns_repository.get_available(["platform_a", "platform_b"])
625
+ self.set_body({"towns": towns})
626
+ ```
627
+
628
+ The repository connects itself using the already-initialized `MongoManager` singleton — the same one used by `self.db`. No need to pass `self.db` or any connection object.
629
+
492
630
  ## 🔄 Routing Convention
493
631
 
494
632
  The framework uses convention over configuration for the routing:
@@ -40,6 +40,7 @@ All available classes and functions:
40
40
  | `FargateTask` | `aws_python_helper.fargate.task_base` | Base class for Fargate tasks |
41
41
  | `FargateExecutor` | `aws_python_helper.fargate.executor` | Launches Fargate tasks from Lambda |
42
42
  | `fargate_handler` | `aws_python_helper.fargate.handler` | Entry point handler for Fargate |
43
+ | `Repository` | `aws_python_helper.repository.base` | Base class for MongoDB repositories |
43
44
  | `MongoJSONEncoder` | `aws_python_helper.utils.json_encoder` | JSON encoder for MongoDB types |
44
45
  | `mongo_json_dumps` | `aws_python_helper.utils.json_encoder` | Helper to serialize MongoDB types |
45
46
  | `serialize_mongo_types` | `aws_python_helper.utils.serializer` | Recursively serialize MongoDB types |
@@ -469,6 +470,143 @@ class AddressAPI(API):
469
470
 
470
471
  `self.external_db` is available in `API`, `SQSConsumer`, `Lambda`, and `FargateTask`.
471
472
 
473
+ ## 🗂️ Repository Pattern
474
+
475
+ The framework provides a `Repository` base class that eliminates repetitive boilerplate in data access layers. Each repository only declares what collection it uses, whether it belongs to an external cluster, and what indexes to create. The base class handles the MongoDB connection and index creation automatically.
476
+
477
+ ### Properties to override
478
+
479
+ | Property | Type | Default | Required |
480
+ |----------|------|---------|----------|
481
+ | `collection_name` | `str` | — | **Yes** |
482
+ | `database_name` | `str` | `"core"` | No |
483
+ | `is_external` | `bool` | `False` | No |
484
+ | `cluster_name` | `str` | `None` | Only if `is_external=True` |
485
+ | `indexes` | `list` | `[]` | No |
486
+
487
+ ### Index format
488
+
489
+ ```python
490
+ @property
491
+ def indexes(self):
492
+ return [
493
+ {"key": [("field", 1)]}, # simple ASC
494
+ {"key": [("field", -1)]}, # simple DESC
495
+ {"key": [("f1", 1), ("f2", -1)], "unique": True}, # compound + unique
496
+ {"key": [("expires_at", 1)], "expireAfterSeconds": 0}, # TTL index
497
+ ]
498
+ ```
499
+
500
+ Indexes are created automatically in the background on first collection access — no need to call any initialization method.
501
+
502
+ ### Repository on the main cluster (`database_name` defaults to `"core"`)
503
+
504
+ ```python
505
+ from aws_python_helper import Repository
506
+
507
+ class TownsRepository(Repository):
508
+
509
+ @property
510
+ def collection_name(self):
511
+ return "towns"
512
+
513
+ @property
514
+ def indexes(self):
515
+ return [
516
+ {"key": [("name", 1)]},
517
+ {"key": [("platform", 1)]},
518
+ ]
519
+
520
+ async def get_available(self, platforms):
521
+ return await self.collection.find(
522
+ {"platform": {"$in": platforms}},
523
+ {"name": 1, "platform": 1}
524
+ ).to_list(length=None)
525
+
526
+ async def find_by_name(self, name):
527
+ return await self.collection.find_one({"name": name})
528
+ ```
529
+
530
+ ### Repository on a different database (not `"core"`)
531
+
532
+ ```python
533
+ from aws_python_helper import Repository
534
+
535
+ class LandRecordsRepository(Repository):
536
+
537
+ @property
538
+ def database_name(self):
539
+ return "land_data"
540
+
541
+ @property
542
+ def collection_name(self):
543
+ return "records"
544
+
545
+ @property
546
+ def indexes(self):
547
+ return [
548
+ {"key": [("unique_id", 1)]},
549
+ {"key": [("owner", 1), ("town", 1)]},
550
+ ]
551
+
552
+ async def bulk_upsert(self, records):
553
+ from pymongo import UpdateOne
554
+ operations = [
555
+ UpdateOne({"unique_id": r["unique_id"]}, {"$set": r}, upsert=True)
556
+ for r in records
557
+ ]
558
+ result = await self.collection.bulk_write(operations)
559
+ return {"upserted": result.upserted_count, "modified": result.modified_count}
560
+ ```
561
+
562
+ ### Repository on an external cluster
563
+
564
+ ```python
565
+ from aws_python_helper import Repository
566
+
567
+ class AddressRepository(Repository):
568
+
569
+ @property
570
+ def database_name(self):
571
+ return "smart_data"
572
+
573
+ @property
574
+ def collection_name(self):
575
+ return "address"
576
+
577
+ @property
578
+ def is_external(self):
579
+ return True
580
+
581
+ @property
582
+ def cluster_name(self):
583
+ return "ClusterDockets" # Must match a name in EXTERNAL_MONGODB_CONNECTIONS
584
+
585
+ async def find_by_query(self, query, limit=None):
586
+ cursor = self.collection.find(query)
587
+ if limit:
588
+ cursor = cursor.limit(limit)
589
+ return await cursor.to_list(length=None)
590
+ ```
591
+
592
+ ### Instantiation — no `db` argument needed
593
+
594
+ ```python
595
+ class MyAPI(API):
596
+
597
+ @property
598
+ def towns_repository(self):
599
+ if not self._towns_repository:
600
+ self._towns_repository = TownsRepository() # no args!
601
+ return self._towns_repository
602
+
603
+ async def process(self):
604
+ towns = await self.towns_repository.get_available(["platform_a", "platform_b"])
605
+ self.set_body({"towns": towns})
606
+ ```
607
+
608
+ The repository connects itself using the already-initialized `MongoManager` singleton — the same one used by `self.db`. No need to pass `self.db` or any connection object.
609
+
472
610
  ## 🔄 Routing Convention
473
611
 
474
612
  The framework uses convention over configuration for the routing:
@@ -24,6 +24,9 @@ from .lambda_standalone.handler import lambda_handler
24
24
  from .utils.json_encoder import MongoJSONEncoder, mongo_json_dumps
25
25
  from .utils.serializer import serialize_mongo_types
26
26
 
27
+ # Repository
28
+ from .repository.base import Repository
29
+
27
30
 
28
31
  __all__ = [
29
32
  'API',
@@ -34,6 +37,7 @@ __all__ = [
34
37
  'FargateTask',
35
38
  'FargateExecutor',
36
39
  'Lambda',
40
+ 'Repository',
37
41
  'fargate_handler',
38
42
  'api_handler',
39
43
  'sqs_handler',
@@ -0,0 +1,3 @@
1
+ from .base import Repository
2
+
3
+ __all__ = ['Repository']
@@ -0,0 +1,188 @@
1
+ """
2
+ Repository Base - Base class for all MongoDB repository classes.
3
+
4
+ Eliminates boilerplate by providing automatic connection management,
5
+ collection access, and index creation without requiring the user to
6
+ pass a database connection or call any initialization method.
7
+ """
8
+
9
+ import asyncio
10
+ import logging
11
+ from abc import ABC, abstractmethod
12
+ from typing import Any, Dict, List, Optional
13
+
14
+ from ..database.mongo_manager import MongoManager
15
+ from ..database.external_mongo_manager import ExternalMongoManager
16
+
17
+
18
+ class Repository(ABC):
19
+ """
20
+ Base class for all MongoDB repositories.
21
+
22
+ Subclasses only need to declare which collection they use,
23
+ whether it is external, and what indexes to create.
24
+ The connection and index creation are handled automatically.
25
+
26
+ Required properties to override:
27
+ collection_name (str): Name of the MongoDB collection.
28
+
29
+ Optional properties to override:
30
+ database_name (str): Name of the database. Default: "core".
31
+ is_external (bool): Whether to use an external cluster. Default: False.
32
+ cluster_name (str): External cluster name. Required if is_external=True.
33
+ indexes (list): List of index definitions to create automatically.
34
+
35
+ Usage:
36
+ class TownsRepository(Repository):
37
+
38
+ @property
39
+ def collection_name(self):
40
+ return "towns"
41
+
42
+ @property
43
+ def indexes(self):
44
+ return [
45
+ {"key": [("name", 1)]},
46
+ {"key": [("platform", 1)]},
47
+ ]
48
+
49
+ async def get_all(self):
50
+ return await self.collection.find({}).to_list(length=None)
51
+
52
+ # Instantiate without passing any db connection
53
+ repo = TownsRepository()
54
+
55
+ External cluster usage:
56
+ class AddressRepository(Repository):
57
+
58
+ @property
59
+ def database_name(self):
60
+ return "smart_data"
61
+
62
+ @property
63
+ def collection_name(self):
64
+ return "address"
65
+
66
+ @property
67
+ def is_external(self):
68
+ return True
69
+
70
+ @property
71
+ def cluster_name(self):
72
+ return "ClusterDockets"
73
+ """
74
+
75
+ def __init__(self):
76
+ self.logger = logging.getLogger(self.__class__.__name__)
77
+ self._indexes_created = False
78
+ self._collection_ref = None
79
+
80
+ @property
81
+ @abstractmethod
82
+ def collection_name(self) -> str:
83
+ """Name of the MongoDB collection."""
84
+ ...
85
+
86
+ @property
87
+ def database_name(self) -> str:
88
+ """Name of the MongoDB database. Default: 'core'."""
89
+ return "core"
90
+
91
+ @property
92
+ def is_external(self) -> bool:
93
+ """Whether this repository uses an external MongoDB cluster. Default: False."""
94
+ return False
95
+
96
+ @property
97
+ def cluster_name(self) -> Optional[str]:
98
+ """
99
+ Name of the external cluster to use.
100
+ Required when is_external=True. Must match a name defined
101
+ in the EXTERNAL_MONGODB_CONNECTIONS environment variable.
102
+ """
103
+ return None
104
+
105
+ @property
106
+ def indexes(self) -> List[Dict[str, Any]]:
107
+ """
108
+ List of index definitions to create automatically on first collection access.
109
+
110
+ Each item is a dict with a required 'key' field and optional Motor kwargs.
111
+
112
+ Example:
113
+ return [
114
+ {"key": [("field", 1)]}, # simple ASC
115
+ {"key": [("field", -1)]}, # simple DESC
116
+ {"key": [("f1", 1), ("f2", -1)], "unique": True}, # compound + unique
117
+ {"key": [("expires_at", 1)], "expireAfterSeconds": 0}, # TTL index
118
+ ]
119
+
120
+ The 'key' value follows pymongo format: list of (field, direction) tuples.
121
+ Any additional keys are passed as kwargs to collection.create_index().
122
+ 'background' defaults to True if not specified.
123
+ """
124
+ return []
125
+
126
+ @property
127
+ def collection(self):
128
+ """
129
+ Lazy-loaded reference to the Motor collection.
130
+
131
+ Resolves the collection from MongoManager (main cluster) or
132
+ ExternalMongoManager (external cluster) based on is_external.
133
+
134
+ On first access, schedules index creation as a background asyncio task
135
+ so indexes are created without blocking the caller.
136
+ """
137
+ if self._collection_ref is None:
138
+ if self.is_external:
139
+ if not self.cluster_name:
140
+ raise ValueError(
141
+ f"{self.__class__.__name__}: 'cluster_name' is required when is_external=True"
142
+ )
143
+ db = ExternalMongoManager.get_database(self.cluster_name, self.database_name)
144
+ else:
145
+ db = MongoManager.get_database(self.database_name)
146
+
147
+ self._collection_ref = db[self.collection_name]
148
+
149
+ # Schedule index creation as a background task on the running event loop.
150
+ # This works because all framework handlers use loop.run_until_complete(),
151
+ # which keeps the loop running while user code executes.
152
+ # create_task() simply adds a coroutine to the existing loop queue
153
+ # without modifying or interrupting it.
154
+ if self.indexes and not self._indexes_created:
155
+ try:
156
+ asyncio.get_running_loop().create_task(self.ensure_indexes())
157
+ except RuntimeError:
158
+ pass # No running event loop (e.g. synchronous test context)
159
+
160
+ return self._collection_ref
161
+
162
+ async def ensure_indexes(self):
163
+ """
164
+ Creates all indexes defined in the `indexes` property.
165
+
166
+ Called automatically in background on first collection access.
167
+ Can also be called explicitly at the start of a method when index
168
+ creation must be guaranteed to complete before proceeding.
169
+
170
+ Idempotent: safe to call multiple times, only runs once.
171
+ """
172
+ if self._indexes_created:
173
+ return
174
+
175
+ for index_def in self.indexes:
176
+ key = index_def.get("key")
177
+ if not key:
178
+ self.logger.warning(f"Index definition missing 'key': {index_def}")
179
+ continue
180
+ options = {k: v for k, v in index_def.items() if k != "key"}
181
+ options.setdefault("background", True)
182
+ try:
183
+ await self.collection.create_index(key, **options)
184
+ self.logger.debug(f"Index created: {key}")
185
+ except Exception as e:
186
+ self.logger.error(f"Error creating index {key}: {e}")
187
+
188
+ self._indexes_created = True
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aws-python-helper
3
- Version: 0.30.1
3
+ Version: 0.31.0
4
4
  Summary: AWS Python Helper Framework
5
5
  Author-email: Fabian Claros <neufabiae@gmail.com>
6
6
  License: MIT
@@ -60,6 +60,7 @@ All available classes and functions:
60
60
  | `FargateTask` | `aws_python_helper.fargate.task_base` | Base class for Fargate tasks |
61
61
  | `FargateExecutor` | `aws_python_helper.fargate.executor` | Launches Fargate tasks from Lambda |
62
62
  | `fargate_handler` | `aws_python_helper.fargate.handler` | Entry point handler for Fargate |
63
+ | `Repository` | `aws_python_helper.repository.base` | Base class for MongoDB repositories |
63
64
  | `MongoJSONEncoder` | `aws_python_helper.utils.json_encoder` | JSON encoder for MongoDB types |
64
65
  | `mongo_json_dumps` | `aws_python_helper.utils.json_encoder` | Helper to serialize MongoDB types |
65
66
  | `serialize_mongo_types` | `aws_python_helper.utils.serializer` | Recursively serialize MongoDB types |
@@ -489,6 +490,143 @@ class AddressAPI(API):
489
490
 
490
491
  `self.external_db` is available in `API`, `SQSConsumer`, `Lambda`, and `FargateTask`.
491
492
 
493
+ ## 🗂️ Repository Pattern
494
+
495
+ The framework provides a `Repository` base class that eliminates repetitive boilerplate in data access layers. Each repository only declares what collection it uses, whether it belongs to an external cluster, and what indexes to create. The base class handles the MongoDB connection and index creation automatically.
496
+
497
+ ### Properties to override
498
+
499
+ | Property | Type | Default | Required |
500
+ |----------|------|---------|----------|
501
+ | `collection_name` | `str` | — | **Yes** |
502
+ | `database_name` | `str` | `"core"` | No |
503
+ | `is_external` | `bool` | `False` | No |
504
+ | `cluster_name` | `str` | `None` | Only if `is_external=True` |
505
+ | `indexes` | `list` | `[]` | No |
506
+
507
+ ### Index format
508
+
509
+ ```python
510
+ @property
511
+ def indexes(self):
512
+ return [
513
+ {"key": [("field", 1)]}, # simple ASC
514
+ {"key": [("field", -1)]}, # simple DESC
515
+ {"key": [("f1", 1), ("f2", -1)], "unique": True}, # compound + unique
516
+ {"key": [("expires_at", 1)], "expireAfterSeconds": 0}, # TTL index
517
+ ]
518
+ ```
519
+
520
+ Indexes are created automatically in the background on first collection access — no need to call any initialization method.
521
+
522
+ ### Repository on the main cluster (`database_name` defaults to `"core"`)
523
+
524
+ ```python
525
+ from aws_python_helper import Repository
526
+
527
+ class TownsRepository(Repository):
528
+
529
+ @property
530
+ def collection_name(self):
531
+ return "towns"
532
+
533
+ @property
534
+ def indexes(self):
535
+ return [
536
+ {"key": [("name", 1)]},
537
+ {"key": [("platform", 1)]},
538
+ ]
539
+
540
+ async def get_available(self, platforms):
541
+ return await self.collection.find(
542
+ {"platform": {"$in": platforms}},
543
+ {"name": 1, "platform": 1}
544
+ ).to_list(length=None)
545
+
546
+ async def find_by_name(self, name):
547
+ return await self.collection.find_one({"name": name})
548
+ ```
549
+
550
+ ### Repository on a different database (not `"core"`)
551
+
552
+ ```python
553
+ from aws_python_helper import Repository
554
+
555
+ class LandRecordsRepository(Repository):
556
+
557
+ @property
558
+ def database_name(self):
559
+ return "land_data"
560
+
561
+ @property
562
+ def collection_name(self):
563
+ return "records"
564
+
565
+ @property
566
+ def indexes(self):
567
+ return [
568
+ {"key": [("unique_id", 1)]},
569
+ {"key": [("owner", 1), ("town", 1)]},
570
+ ]
571
+
572
+ async def bulk_upsert(self, records):
573
+ from pymongo import UpdateOne
574
+ operations = [
575
+ UpdateOne({"unique_id": r["unique_id"]}, {"$set": r}, upsert=True)
576
+ for r in records
577
+ ]
578
+ result = await self.collection.bulk_write(operations)
579
+ return {"upserted": result.upserted_count, "modified": result.modified_count}
580
+ ```
581
+
582
+ ### Repository on an external cluster
583
+
584
+ ```python
585
+ from aws_python_helper import Repository
586
+
587
+ class AddressRepository(Repository):
588
+
589
+ @property
590
+ def database_name(self):
591
+ return "smart_data"
592
+
593
+ @property
594
+ def collection_name(self):
595
+ return "address"
596
+
597
+ @property
598
+ def is_external(self):
599
+ return True
600
+
601
+ @property
602
+ def cluster_name(self):
603
+ return "ClusterDockets" # Must match a name in EXTERNAL_MONGODB_CONNECTIONS
604
+
605
+ async def find_by_query(self, query, limit=None):
606
+ cursor = self.collection.find(query)
607
+ if limit:
608
+ cursor = cursor.limit(limit)
609
+ return await cursor.to_list(length=None)
610
+ ```
611
+
612
+ ### Instantiation — no `db` argument needed
613
+
614
+ ```python
615
+ class MyAPI(API):
616
+
617
+ @property
618
+ def towns_repository(self):
619
+ if not self._towns_repository:
620
+ self._towns_repository = TownsRepository() # no args!
621
+ return self._towns_repository
622
+
623
+ async def process(self):
624
+ towns = await self.towns_repository.get_available(["platform_a", "platform_b"])
625
+ self.set_body({"towns": towns})
626
+ ```
627
+
628
+ The repository connects itself using the already-initialized `MongoManager` singleton — the same one used by `self.db`. No need to pass `self.db` or any connection object.
629
+
492
630
  ## 🔄 Routing Convention
493
631
 
494
632
  The framework uses convention over configuration for the routing:
@@ -28,6 +28,8 @@ aws_python_helper/lambda_standalone/__init__.py
28
28
  aws_python_helper/lambda_standalone/base.py
29
29
  aws_python_helper/lambda_standalone/fetcher.py
30
30
  aws_python_helper/lambda_standalone/handler.py
31
+ aws_python_helper/repository/__init__.py
32
+ aws_python_helper/repository/base.py
31
33
  aws_python_helper/sns/__init__.py
32
34
  aws_python_helper/sns/publisher.py
33
35
  aws_python_helper/sqs/__init__.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "aws-python-helper"
7
- version = "0.30.1"
7
+ version = "0.31.0"
8
8
  description = "AWS Python Helper Framework"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"