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.
- {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/PKG-INFO +139 -1
- {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/README.md +138 -0
- {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/__init__.py +4 -0
- aws_python_helper-0.31.0/aws_python_helper/repository/__init__.py +3 -0
- aws_python_helper-0.31.0/aws_python_helper/repository/base.py +188 -0
- {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper.egg-info/PKG-INFO +139 -1
- {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper.egg-info/SOURCES.txt +2 -0
- {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/pyproject.toml +1 -1
- {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/api/__init__.py +0 -0
- {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/api/auth_middleware.py +0 -0
- {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/api/auth_validators.py +0 -0
- {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/api/base.py +0 -0
- {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/api/dispatcher.py +0 -0
- {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/api/exceptions.py +0 -0
- {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/api/fetcher.py +0 -0
- {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/api/handler.py +0 -0
- {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/database/__init__.py +0 -0
- {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/database/database_proxy.py +0 -0
- {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/database/external_database_proxy.py +0 -0
- {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/database/external_mongo_manager.py +0 -0
- {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/database/mongo_manager.py +0 -0
- {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/fargate/__init__.py +0 -0
- {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/fargate/executor.py +0 -0
- {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/fargate/fetcher.py +0 -0
- {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/fargate/handler.py +0 -0
- {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/fargate/task_base.py +0 -0
- {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/lambda_standalone/__init__.py +0 -0
- {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/lambda_standalone/base.py +0 -0
- {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/lambda_standalone/fetcher.py +0 -0
- {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/lambda_standalone/handler.py +0 -0
- {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/sns/__init__.py +0 -0
- {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/sns/publisher.py +0 -0
- {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/sqs/__init__.py +0 -0
- {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/sqs/consumer_base.py +0 -0
- {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/sqs/fetcher.py +0 -0
- {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/sqs/handler.py +0 -0
- {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/utils/__init__.py +0 -0
- {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/utils/json_encoder.py +0 -0
- {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/utils/response.py +0 -0
- {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/utils/serializer.py +0 -0
- {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper.egg-info/dependency_links.txt +0 -0
- {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper.egg-info/requires.txt +0 -0
- {aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper.egg-info/top_level.txt +0 -0
- {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.
|
|
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,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.
|
|
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:
|
{aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper.egg-info/SOURCES.txt
RENAMED
|
@@ -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
|
|
File without changes
|
{aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/api/auth_middleware.py
RENAMED
|
File without changes
|
{aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/api/auth_validators.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/database/__init__.py
RENAMED
|
File without changes
|
{aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/database/database_proxy.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/database/mongo_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/fargate/task_base.py
RENAMED
|
File without changes
|
|
File without changes
|
{aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/lambda_standalone/base.py
RENAMED
|
File without changes
|
{aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/lambda_standalone/fetcher.py
RENAMED
|
File without changes
|
{aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/lambda_standalone/handler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/sqs/consumer_base.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper/utils/json_encoder.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper.egg-info/requires.txt
RENAMED
|
File without changes
|
{aws_python_helper-0.30.1 → aws_python_helper-0.31.0}/aws_python_helper.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|