aws-python-helper 1.0.2__tar.gz → 1.0.3__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-1.0.2 → aws_python_helper-1.0.3}/PKG-INFO +30 -12
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/README.md +29 -11
- aws_python_helper-1.0.3/aws_python_helper/model_index_sync/base.py +191 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/repository/base.py +68 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper.egg-info/PKG-INFO +30 -12
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/pyproject.toml +1 -1
- aws_python_helper-1.0.2/aws_python_helper/model_index_sync/base.py +0 -114
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/__init__.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/api/__init__.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/api/auth_middleware.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/api/auth_validators.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/api/base.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/api/dispatcher.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/api/exceptions.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/api/fetcher.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/api/handler.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/context/__init__.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/context/session.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/context/state_validator.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/database/__init__.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/database/database_proxy.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/database/external_database_proxy.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/database/external_mongo_manager.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/database/mongo_manager.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/fargate/__init__.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/fargate/executor.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/fargate/fetcher.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/fargate/handler.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/fargate/task_base.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/invoker/__init__.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/invoker/api_client.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/invoker/exceptions.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/invoker/lambda_invoker.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/lambda_standalone/__init__.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/lambda_standalone/base.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/lambda_standalone/fetcher.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/lambda_standalone/handler.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/model_index_sync/__init__.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/model_query/__init__.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/model_query/base.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/repository/__init__.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/sns/__init__.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/sns/publisher.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/sqs/__init__.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/sqs/consumer_base.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/sqs/fetcher.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/sqs/handler.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/utils/__init__.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/utils/json_encoder.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/utils/response.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/utils/serializer.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper.egg-info/SOURCES.txt +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper.egg-info/dependency_links.txt +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper.egg-info/requires.txt +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper.egg-info/top_level.txt +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aws-python-helper
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.3
|
|
4
4
|
Summary: AWS Python Helper Framework
|
|
5
5
|
Author-email: Fabian Claros <neufabiae@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -39,7 +39,7 @@ Mini-framework to create REST APIs, SQS Consumers, SNS Publishers, Fargate Tasks
|
|
|
39
39
|
- **LambdaInvoker**: Invoke other Lambda functions (sync or async) with built-in error handling
|
|
40
40
|
- **ApiClient**: HTTP client for inter-service communication — resolves service URLs and auth token from environment variables automatically
|
|
41
41
|
- **ModelQueryLambda**: Base class for MongoDB query-proxy Lambdas — declare an `allowed_collections` whitelist and the framework handles `find` and `aggregate` operations generically
|
|
42
|
-
- **ModelIndexSyncLambda**: Base class for index-synchronization Lambdas — declare your `repositories` and the framework creates
|
|
42
|
+
- **ModelIndexSyncLambda**: Base class for index-synchronization Lambdas — declare your `repositories` and the framework creates the declared indexes **and prunes orphans** across the `core` database and all active state databases, idempotently and in parallel, in a single run (run once per deploy)
|
|
43
43
|
- **JSON utilities**: Automatic serialization of MongoDB types
|
|
44
44
|
- **Type hints**: Modern Python with type annotations
|
|
45
45
|
- **Async/await**: Full support for asynchronous operations
|
|
@@ -68,7 +68,7 @@ All available classes and functions:
|
|
|
68
68
|
| `fargate_handler` | `aws_python_helper.fargate.handler` | Entry point handler for Fargate |
|
|
69
69
|
| `Repository` | `aws_python_helper.repository.base` | Base class for MongoDB repositories |
|
|
70
70
|
| `ModelQueryLambda` | `aws_python_helper` | Generic MongoDB query-proxy Lambda base class |
|
|
71
|
-
| `ModelIndexSyncLambda` | `aws_python_helper` | Index-sync Lambda base class (creates indexes across `core` + all state DBs) |
|
|
71
|
+
| `ModelIndexSyncLambda` | `aws_python_helper` | Index-sync Lambda base class (creates declared indexes + prunes orphans across `core` + all state DBs, in parallel) |
|
|
72
72
|
| `LambdaInvoker` | `aws_python_helper` | Invoke Lambda functions (sync/async) with error handling |
|
|
73
73
|
| `ApiClient` | `aws_python_helper` | HTTP client for inter-service communication |
|
|
74
74
|
| `LambdaInvocationError` | `aws_python_helper` | Raised when boto3 fails to invoke a Lambda |
|
|
@@ -1360,10 +1360,16 @@ Always pass `self.session.to_dict()` when calling from an API or Lambda to ensur
|
|
|
1360
1360
|
|
|
1361
1361
|
## 🧱 ModelIndexSyncLambda
|
|
1362
1362
|
|
|
1363
|
-
`ModelIndexSyncLambda` is a ready-to-use base class for **
|
|
1363
|
+
`ModelIndexSyncLambda` is a ready-to-use base class for **synchronizing all of a service's MongoDB indexes deterministically**, in one run. Because index creation is no longer triggered at request time (see the [Repository Pattern](#️-repository-pattern) note), this Lambda is the single source of truth for materializing indexes — typically invoked **once per deploy**.
|
|
1364
1364
|
|
|
1365
1365
|
**Why:** in short-lived Lambda/API runtimes, creating indexes lazily on first access often never finishes (the function responds and freezes). This Lambda creates every declared index up front, `await`-ing each one, idempotently — and across **every** active state database, not just the one of the current request.
|
|
1366
1366
|
|
|
1367
|
+
It does a **two-way sync** per collection: it creates the declared indexes **and prunes orphans** — any index present in MongoDB that is no longer declared in the repository's `indexes` is dropped (the default `_id_` index and special text/geo/hashed indexes are always preserved).
|
|
1368
|
+
|
|
1369
|
+
> ⚠️ **Orphan pruning drops indexes not declared in code.** Indexes created out-of-band (by a DBA, Atlas, or another tool) that aren't in any repository's `indexes` will be removed on every run. Declare every index you want to keep.
|
|
1370
|
+
|
|
1371
|
+
**Concurrency & fault tolerance:** each `(collection, database)` unit is synced **in parallel** (bounded by `max_concurrency`, default 10); index work *within* a unit stays sequential. A failure in one unit is **captured and isolated** — it never aborts the others, so a single broken collection doesn't stop the rest of the sync. The top-level response is always `success: true`; per-unit outcomes are reported in `results[].success`.
|
|
1372
|
+
|
|
1367
1373
|
### Creating an index-sync Lambda
|
|
1368
1374
|
|
|
1369
1375
|
Create `src/lambda/model-index-sync/main.py` and declare which repositories to sync:
|
|
@@ -1409,14 +1415,20 @@ This Lambda sets `requires_state = False`: it is not tied to a single state and
|
|
|
1409
1415
|
|
|
1410
1416
|
### Response format
|
|
1411
1417
|
|
|
1418
|
+
The top-level `success` is **always `true`** (a failing collection no longer aborts the run). Inspect `data.summary` for the OK/failed counts and `data.results[].success` for each unit; failed units include an `error` field.
|
|
1419
|
+
|
|
1412
1420
|
```json
|
|
1413
1421
|
{
|
|
1414
1422
|
"success": true,
|
|
1415
|
-
"data":
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
]
|
|
1423
|
+
"data": {
|
|
1424
|
+
"summary": { "total": 4, "succeeded": 3, "failed": 1 },
|
|
1425
|
+
"results": [
|
|
1426
|
+
{"repository": "ParcelsRepository", "database": "connecticut", "collection": "parcels", "indexes": ["parcel_id_1", "..."], "orphan_indexes_dropped": ["old_field_1"], "success": true},
|
|
1427
|
+
{"repository": "ParcelsRepository", "database": "new_jersey", "collection": "parcels", "indexes": ["parcel_id_1", "..."], "orphan_indexes_dropped": [], "success": true},
|
|
1428
|
+
{"repository": "UsersRepository", "database": "core", "collection": "users", "indexes": ["email_1"], "orphan_indexes_dropped": [], "success": true},
|
|
1429
|
+
{"repository": "TokensRepository", "database": "core", "collection": "tokens", "indexes": [], "orphan_indexes_dropped": [], "success": false, "error": "..."}
|
|
1430
|
+
]
|
|
1431
|
+
}
|
|
1420
1432
|
}
|
|
1421
1433
|
```
|
|
1422
1434
|
|
|
@@ -1427,11 +1439,14 @@ This Lambda sets `requires_state = False`: it is not tied to a single state and
|
|
|
1427
1439
|
| `repositories` | `[]` | **Required.** List of `Repository` subclasses (the classes, not instances) to sync. Empty raises a validation error |
|
|
1428
1440
|
| `states_database` | `"core"` | Database holding the states collection |
|
|
1429
1441
|
| `states_collection` | `"states"` | Collection listing the states (filtered by `is_active = true`, field `name`) |
|
|
1442
|
+
| `max_concurrency` | `10` | Max `(collection, database)` units synced in parallel. `0` (or negative) = unlimited |
|
|
1430
1443
|
| `requires_state` | `False` | Inherited override — the Lambda runs across all states, no session required |
|
|
1431
1444
|
|
|
1432
1445
|
### Running it once per deploy (Terraform)
|
|
1433
1446
|
|
|
1434
|
-
Since deploys are applied manually with Terraform, attach an `aws_lambda_invocation` that runs **on every `apply`**, right after the function code is updated. `timestamp()` changes on every plan, so it always re-runs (creation is idempotent and also picks up any newly-activated state).
|
|
1447
|
+
Since deploys are applied manually with Terraform, attach an `aws_lambda_invocation` that runs **on every `apply`**, right after the function code is updated. `timestamp()` changes on every plan, so it always re-runs (creation is idempotent and also picks up any newly-activated state).
|
|
1448
|
+
|
|
1449
|
+
> **Detecting failures in the deploy.** The top-level `success` is now **always `true`** — a single failed collection no longer aborts the run, so checking `success` alone won't catch partial failures. If you want the `apply` to fail when any collection failed, point the `postcondition` at `data.summary.failed` instead (shown below). If you'd rather the deploy never fail on index errors and only review them afterward (in the response/logs), simply omit the `lifecycle` block.
|
|
1435
1450
|
|
|
1436
1451
|
```hcl
|
|
1437
1452
|
module "lambda" {
|
|
@@ -1449,10 +1464,13 @@ resource "aws_lambda_invocation" "sync_on_deploy" {
|
|
|
1449
1464
|
|
|
1450
1465
|
depends_on = [module.lambda] # run after the code is updated
|
|
1451
1466
|
|
|
1467
|
+
# Optional: fail the apply if any collection failed to sync.
|
|
1468
|
+
# Omit this whole lifecycle block if you prefer the deploy to never fail on
|
|
1469
|
+
# index errors (review data.results[].success / logs instead).
|
|
1452
1470
|
lifecycle {
|
|
1453
1471
|
postcondition {
|
|
1454
|
-
condition = try(jsondecode(self.result).
|
|
1455
|
-
error_message = "Index sync
|
|
1472
|
+
condition = try(jsondecode(self.result).data.summary.failed, 1) == 0
|
|
1473
|
+
error_message = "Index sync had failures during apply: ${self.result}"
|
|
1456
1474
|
}
|
|
1457
1475
|
}
|
|
1458
1476
|
}
|
|
@@ -19,7 +19,7 @@ Mini-framework to create REST APIs, SQS Consumers, SNS Publishers, Fargate Tasks
|
|
|
19
19
|
- **LambdaInvoker**: Invoke other Lambda functions (sync or async) with built-in error handling
|
|
20
20
|
- **ApiClient**: HTTP client for inter-service communication — resolves service URLs and auth token from environment variables automatically
|
|
21
21
|
- **ModelQueryLambda**: Base class for MongoDB query-proxy Lambdas — declare an `allowed_collections` whitelist and the framework handles `find` and `aggregate` operations generically
|
|
22
|
-
- **ModelIndexSyncLambda**: Base class for index-synchronization Lambdas — declare your `repositories` and the framework creates
|
|
22
|
+
- **ModelIndexSyncLambda**: Base class for index-synchronization Lambdas — declare your `repositories` and the framework creates the declared indexes **and prunes orphans** across the `core` database and all active state databases, idempotently and in parallel, in a single run (run once per deploy)
|
|
23
23
|
- **JSON utilities**: Automatic serialization of MongoDB types
|
|
24
24
|
- **Type hints**: Modern Python with type annotations
|
|
25
25
|
- **Async/await**: Full support for asynchronous operations
|
|
@@ -48,7 +48,7 @@ All available classes and functions:
|
|
|
48
48
|
| `fargate_handler` | `aws_python_helper.fargate.handler` | Entry point handler for Fargate |
|
|
49
49
|
| `Repository` | `aws_python_helper.repository.base` | Base class for MongoDB repositories |
|
|
50
50
|
| `ModelQueryLambda` | `aws_python_helper` | Generic MongoDB query-proxy Lambda base class |
|
|
51
|
-
| `ModelIndexSyncLambda` | `aws_python_helper` | Index-sync Lambda base class (creates indexes across `core` + all state DBs) |
|
|
51
|
+
| `ModelIndexSyncLambda` | `aws_python_helper` | Index-sync Lambda base class (creates declared indexes + prunes orphans across `core` + all state DBs, in parallel) |
|
|
52
52
|
| `LambdaInvoker` | `aws_python_helper` | Invoke Lambda functions (sync/async) with error handling |
|
|
53
53
|
| `ApiClient` | `aws_python_helper` | HTTP client for inter-service communication |
|
|
54
54
|
| `LambdaInvocationError` | `aws_python_helper` | Raised when boto3 fails to invoke a Lambda |
|
|
@@ -1340,10 +1340,16 @@ Always pass `self.session.to_dict()` when calling from an API or Lambda to ensur
|
|
|
1340
1340
|
|
|
1341
1341
|
## 🧱 ModelIndexSyncLambda
|
|
1342
1342
|
|
|
1343
|
-
`ModelIndexSyncLambda` is a ready-to-use base class for **
|
|
1343
|
+
`ModelIndexSyncLambda` is a ready-to-use base class for **synchronizing all of a service's MongoDB indexes deterministically**, in one run. Because index creation is no longer triggered at request time (see the [Repository Pattern](#️-repository-pattern) note), this Lambda is the single source of truth for materializing indexes — typically invoked **once per deploy**.
|
|
1344
1344
|
|
|
1345
1345
|
**Why:** in short-lived Lambda/API runtimes, creating indexes lazily on first access often never finishes (the function responds and freezes). This Lambda creates every declared index up front, `await`-ing each one, idempotently — and across **every** active state database, not just the one of the current request.
|
|
1346
1346
|
|
|
1347
|
+
It does a **two-way sync** per collection: it creates the declared indexes **and prunes orphans** — any index present in MongoDB that is no longer declared in the repository's `indexes` is dropped (the default `_id_` index and special text/geo/hashed indexes are always preserved).
|
|
1348
|
+
|
|
1349
|
+
> ⚠️ **Orphan pruning drops indexes not declared in code.** Indexes created out-of-band (by a DBA, Atlas, or another tool) that aren't in any repository's `indexes` will be removed on every run. Declare every index you want to keep.
|
|
1350
|
+
|
|
1351
|
+
**Concurrency & fault tolerance:** each `(collection, database)` unit is synced **in parallel** (bounded by `max_concurrency`, default 10); index work *within* a unit stays sequential. A failure in one unit is **captured and isolated** — it never aborts the others, so a single broken collection doesn't stop the rest of the sync. The top-level response is always `success: true`; per-unit outcomes are reported in `results[].success`.
|
|
1352
|
+
|
|
1347
1353
|
### Creating an index-sync Lambda
|
|
1348
1354
|
|
|
1349
1355
|
Create `src/lambda/model-index-sync/main.py` and declare which repositories to sync:
|
|
@@ -1389,14 +1395,20 @@ This Lambda sets `requires_state = False`: it is not tied to a single state and
|
|
|
1389
1395
|
|
|
1390
1396
|
### Response format
|
|
1391
1397
|
|
|
1398
|
+
The top-level `success` is **always `true`** (a failing collection no longer aborts the run). Inspect `data.summary` for the OK/failed counts and `data.results[].success` for each unit; failed units include an `error` field.
|
|
1399
|
+
|
|
1392
1400
|
```json
|
|
1393
1401
|
{
|
|
1394
1402
|
"success": true,
|
|
1395
|
-
"data":
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
]
|
|
1403
|
+
"data": {
|
|
1404
|
+
"summary": { "total": 4, "succeeded": 3, "failed": 1 },
|
|
1405
|
+
"results": [
|
|
1406
|
+
{"repository": "ParcelsRepository", "database": "connecticut", "collection": "parcels", "indexes": ["parcel_id_1", "..."], "orphan_indexes_dropped": ["old_field_1"], "success": true},
|
|
1407
|
+
{"repository": "ParcelsRepository", "database": "new_jersey", "collection": "parcels", "indexes": ["parcel_id_1", "..."], "orphan_indexes_dropped": [], "success": true},
|
|
1408
|
+
{"repository": "UsersRepository", "database": "core", "collection": "users", "indexes": ["email_1"], "orphan_indexes_dropped": [], "success": true},
|
|
1409
|
+
{"repository": "TokensRepository", "database": "core", "collection": "tokens", "indexes": [], "orphan_indexes_dropped": [], "success": false, "error": "..."}
|
|
1410
|
+
]
|
|
1411
|
+
}
|
|
1400
1412
|
}
|
|
1401
1413
|
```
|
|
1402
1414
|
|
|
@@ -1407,11 +1419,14 @@ This Lambda sets `requires_state = False`: it is not tied to a single state and
|
|
|
1407
1419
|
| `repositories` | `[]` | **Required.** List of `Repository` subclasses (the classes, not instances) to sync. Empty raises a validation error |
|
|
1408
1420
|
| `states_database` | `"core"` | Database holding the states collection |
|
|
1409
1421
|
| `states_collection` | `"states"` | Collection listing the states (filtered by `is_active = true`, field `name`) |
|
|
1422
|
+
| `max_concurrency` | `10` | Max `(collection, database)` units synced in parallel. `0` (or negative) = unlimited |
|
|
1410
1423
|
| `requires_state` | `False` | Inherited override — the Lambda runs across all states, no session required |
|
|
1411
1424
|
|
|
1412
1425
|
### Running it once per deploy (Terraform)
|
|
1413
1426
|
|
|
1414
|
-
Since deploys are applied manually with Terraform, attach an `aws_lambda_invocation` that runs **on every `apply`**, right after the function code is updated. `timestamp()` changes on every plan, so it always re-runs (creation is idempotent and also picks up any newly-activated state).
|
|
1427
|
+
Since deploys are applied manually with Terraform, attach an `aws_lambda_invocation` that runs **on every `apply`**, right after the function code is updated. `timestamp()` changes on every plan, so it always re-runs (creation is idempotent and also picks up any newly-activated state).
|
|
1428
|
+
|
|
1429
|
+
> **Detecting failures in the deploy.** The top-level `success` is now **always `true`** — a single failed collection no longer aborts the run, so checking `success` alone won't catch partial failures. If you want the `apply` to fail when any collection failed, point the `postcondition` at `data.summary.failed` instead (shown below). If you'd rather the deploy never fail on index errors and only review them afterward (in the response/logs), simply omit the `lifecycle` block.
|
|
1415
1430
|
|
|
1416
1431
|
```hcl
|
|
1417
1432
|
module "lambda" {
|
|
@@ -1429,10 +1444,13 @@ resource "aws_lambda_invocation" "sync_on_deploy" {
|
|
|
1429
1444
|
|
|
1430
1445
|
depends_on = [module.lambda] # run after the code is updated
|
|
1431
1446
|
|
|
1447
|
+
# Optional: fail the apply if any collection failed to sync.
|
|
1448
|
+
# Omit this whole lifecycle block if you prefer the deploy to never fail on
|
|
1449
|
+
# index errors (review data.results[].success / logs instead).
|
|
1432
1450
|
lifecycle {
|
|
1433
1451
|
postcondition {
|
|
1434
|
-
condition = try(jsondecode(self.result).
|
|
1435
|
-
error_message = "Index sync
|
|
1452
|
+
condition = try(jsondecode(self.result).data.summary.failed, 1) == 0
|
|
1453
|
+
error_message = "Index sync had failures during apply: ${self.result}"
|
|
1436
1454
|
}
|
|
1437
1455
|
}
|
|
1438
1456
|
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ModelIndexSyncLambda - Synchronizes MongoDB indexes for a service's repositories.
|
|
3
|
+
|
|
4
|
+
Creates (idempotently) every index declared in the service's repositories, in
|
|
5
|
+
the correct databases:
|
|
6
|
+
- Repositories with a fixed database_key (e.g. 'core') → that database.
|
|
7
|
+
- State-scoped repositories (database_key is None) → every active state
|
|
8
|
+
database, read from core.states (is_active=True). A state-scoped collection
|
|
9
|
+
(e.g. 'parcels') therefore gets its indexes created in 'connecticut',
|
|
10
|
+
'new_jersey', etc.
|
|
11
|
+
|
|
12
|
+
It also prunes orphan indexes: any index present in MongoDB but no longer
|
|
13
|
+
declared in a repository's `indexes` is dropped (see Repository.prune_orphan_indexes).
|
|
14
|
+
|
|
15
|
+
Designed to run once per deploy (and on demand), guaranteeing index creation
|
|
16
|
+
without the per-request overhead of creating indexes at runtime.
|
|
17
|
+
|
|
18
|
+
Each (collection, database) unit is synced concurrently (bounded by
|
|
19
|
+
`max_concurrency`); index creation WITHIN a unit stays sequential. A failure in
|
|
20
|
+
one unit is captured and never aborts the others, so a single broken collection
|
|
21
|
+
does not stop the rest of the sync — the failure is reported instead.
|
|
22
|
+
|
|
23
|
+
Usage — subclass and declare the repositories to sync:
|
|
24
|
+
|
|
25
|
+
import aws_python_helper
|
|
26
|
+
from repositories.parcels import ParcelsRepository
|
|
27
|
+
from repositories.users import UsersRepository
|
|
28
|
+
|
|
29
|
+
class PropertiesModelIndexSyncLambda(aws_python_helper.ModelIndexSyncLambda):
|
|
30
|
+
@property
|
|
31
|
+
def repositories(self) -> list:
|
|
32
|
+
return [ParcelsRepository, UsersRepository]
|
|
33
|
+
|
|
34
|
+
This Lambda does not require a session state (requires_state = False); it
|
|
35
|
+
operates across all active states.
|
|
36
|
+
|
|
37
|
+
Response (via Lambda base run()) — always success=True at the top level; per-unit
|
|
38
|
+
outcome is reported in results[].success:
|
|
39
|
+
{"success": True, "data": {
|
|
40
|
+
"summary": {"total": 3, "succeeded": 2, "failed": 1},
|
|
41
|
+
"results": [
|
|
42
|
+
{"repository": "ParcelsRepository", "database": "connecticut",
|
|
43
|
+
"collection": "parcels", "indexes": ["parcel_id_1", ...],
|
|
44
|
+
"orphan_indexes_dropped": ["old_field_1"], "success": True},
|
|
45
|
+
{"repository": "UsersRepository", "database": "core",
|
|
46
|
+
"collection": "users", "indexes": ["email_1"],
|
|
47
|
+
"orphan_indexes_dropped": [], "success": True},
|
|
48
|
+
{"repository": "TokensRepository", "database": "core",
|
|
49
|
+
"collection": "tokens", "indexes": [], "orphan_indexes_dropped": [],
|
|
50
|
+
"success": False, "error": "..."},
|
|
51
|
+
]
|
|
52
|
+
}}
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
import asyncio
|
|
56
|
+
from typing import Any, List, Type
|
|
57
|
+
|
|
58
|
+
from ..lambda_standalone.base import Lambda
|
|
59
|
+
from ..repository.base import Repository
|
|
60
|
+
from ..database.mongo_manager import MongoManager
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ModelIndexSyncLambda(Lambda):
|
|
64
|
+
"""
|
|
65
|
+
Base class for index-synchronization Lambdas.
|
|
66
|
+
|
|
67
|
+
Subclass and override `repositories` to declare which repositories to sync.
|
|
68
|
+
Everything else (resolving core vs state-scoped, reading active states and
|
|
69
|
+
creating the indexes) is handled automatically.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def requires_state(self) -> bool:
|
|
74
|
+
# Operates across all states; not tied to a single one.
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def repositories(self) -> List[Type[Repository]]:
|
|
79
|
+
"""
|
|
80
|
+
Repositories whose indexes should be synchronized.
|
|
81
|
+
|
|
82
|
+
Override returning a list of Repository subclasses (the classes, not
|
|
83
|
+
instances). Declare them explicitly rather than auto-discovering, so the
|
|
84
|
+
synced set is intentional and nothing is silently missed or added.
|
|
85
|
+
"""
|
|
86
|
+
return []
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def states_database(self) -> str:
|
|
90
|
+
"""Database holding the states collection. Default: 'core'."""
|
|
91
|
+
return "core"
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def states_collection(self) -> str:
|
|
95
|
+
"""Collection listing the states. Default: 'states'."""
|
|
96
|
+
return "states"
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def max_concurrency(self) -> int:
|
|
100
|
+
"""
|
|
101
|
+
Max number of (collection, database) index syncs running in parallel.
|
|
102
|
+
0 (or negative) means unlimited. Default: 10.
|
|
103
|
+
"""
|
|
104
|
+
return 10
|
|
105
|
+
|
|
106
|
+
async def validate(self):
|
|
107
|
+
if not self.repositories:
|
|
108
|
+
raise ValueError(
|
|
109
|
+
"repositories is not configured on this ModelIndexSyncLambda"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
async def _active_states(self) -> List[str]:
|
|
113
|
+
"""Names of active states — i.e. the state-scoped database names."""
|
|
114
|
+
db = MongoManager.get_database(self.states_database)
|
|
115
|
+
cursor = db[self.states_collection].find(
|
|
116
|
+
{"is_active": True}, {"name": 1, "_id": 0}
|
|
117
|
+
)
|
|
118
|
+
return [doc["name"] async for doc in cursor if doc.get("name")]
|
|
119
|
+
|
|
120
|
+
async def _sync_target(self, repo_cls: Type[Repository], db_name: str) -> dict:
|
|
121
|
+
"""
|
|
122
|
+
Sync one collection in one database: create declared indexes and drop
|
|
123
|
+
orphan ones.
|
|
124
|
+
|
|
125
|
+
Captures its own errors so a single failure never aborts the rest of the
|
|
126
|
+
sync — a failed unit returns success=False with the error message, and
|
|
127
|
+
any indexes created before the failure are still reported.
|
|
128
|
+
"""
|
|
129
|
+
result = {
|
|
130
|
+
"repository": repo_cls.__name__,
|
|
131
|
+
"database": db_name,
|
|
132
|
+
"collection": None,
|
|
133
|
+
"indexes": [],
|
|
134
|
+
"orphan_indexes_dropped": [],
|
|
135
|
+
"success": True,
|
|
136
|
+
}
|
|
137
|
+
try:
|
|
138
|
+
repo = repo_cls()
|
|
139
|
+
result["collection"] = repo.collection_name
|
|
140
|
+
result["indexes"] = await repo.ensure_indexes(database_name=db_name)
|
|
141
|
+
result["orphan_indexes_dropped"] = await repo.prune_orphan_indexes(
|
|
142
|
+
database_name=db_name, dry_run=False
|
|
143
|
+
)
|
|
144
|
+
except Exception as e:
|
|
145
|
+
self.logger.error(
|
|
146
|
+
f"Index sync failed for {repo_cls.__name__} on '{db_name}': {e}"
|
|
147
|
+
)
|
|
148
|
+
result["success"] = False
|
|
149
|
+
result["error"] = str(e)
|
|
150
|
+
return result
|
|
151
|
+
|
|
152
|
+
async def process(self) -> Any:
|
|
153
|
+
active_states = await self._active_states()
|
|
154
|
+
|
|
155
|
+
# Plan all independent (repo_cls, db_name) work units.
|
|
156
|
+
targets: List[tuple] = []
|
|
157
|
+
for repo_cls in self.repositories:
|
|
158
|
+
repo = repo_cls()
|
|
159
|
+
# database_key is None -> state-scoped (collection repeats per state db).
|
|
160
|
+
is_state_scoped = repo.database_key is None and not repo.is_external
|
|
161
|
+
target_databases = active_states if is_state_scoped else [repo.database_name]
|
|
162
|
+
for db_name in target_databases:
|
|
163
|
+
targets.append((repo_cls, db_name))
|
|
164
|
+
|
|
165
|
+
# Each (collection, database) runs concurrently; index creation WITHIN
|
|
166
|
+
# each one stays sequential (via await). _sync_target captures its own
|
|
167
|
+
# errors, so one failed unit never aborts the others.
|
|
168
|
+
limit = self.max_concurrency
|
|
169
|
+
if limit and limit > 0:
|
|
170
|
+
semaphore = asyncio.Semaphore(limit)
|
|
171
|
+
|
|
172
|
+
async def run(repo_cls, db_name):
|
|
173
|
+
async with semaphore:
|
|
174
|
+
return await self._sync_target(repo_cls, db_name)
|
|
175
|
+
else:
|
|
176
|
+
async def run(repo_cls, db_name):
|
|
177
|
+
return await self._sync_target(repo_cls, db_name)
|
|
178
|
+
|
|
179
|
+
results = list(await asyncio.gather(
|
|
180
|
+
*(run(repo_cls, db_name) for repo_cls, db_name in targets)
|
|
181
|
+
))
|
|
182
|
+
|
|
183
|
+
failed = [r for r in results if not r["success"]]
|
|
184
|
+
return {
|
|
185
|
+
"summary": {
|
|
186
|
+
"total": len(results),
|
|
187
|
+
"succeeded": len(results) - len(failed),
|
|
188
|
+
"failed": len(failed),
|
|
189
|
+
},
|
|
190
|
+
"results": results,
|
|
191
|
+
}
|
|
@@ -255,3 +255,71 @@ class Repository(ABC):
|
|
|
255
255
|
|
|
256
256
|
self._indexes_created[key] = True
|
|
257
257
|
return created
|
|
258
|
+
|
|
259
|
+
async def prune_orphan_indexes(
|
|
260
|
+
self, database_name: Optional[str] = None, dry_run: bool = True
|
|
261
|
+
) -> List[str]:
|
|
262
|
+
"""
|
|
263
|
+
Detects and (optionally) drops indexes that exist in MongoDB but are NOT
|
|
264
|
+
declared in the `indexes` property — i.e. orphan indexes left behind
|
|
265
|
+
after an index was removed from the code.
|
|
266
|
+
|
|
267
|
+
Matching is done by index key spec (fields + direction), never by name,
|
|
268
|
+
since index names are auto-generated by MongoDB. The default '_id_' index
|
|
269
|
+
is always preserved, and special indexes (text/geo/hashed, whose key spec
|
|
270
|
+
MongoDB rewrites internally) are skipped to avoid false positives.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
database_name: Target database. Defaults to the resolved
|
|
274
|
+
database_name (database_key or current session state).
|
|
275
|
+
dry_run: If True (default), only reports orphans without dropping.
|
|
276
|
+
If False, drops each orphan index.
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
List of orphan index names (dropped when dry_run=False).
|
|
280
|
+
"""
|
|
281
|
+
db_name = database_name or self.database_name
|
|
282
|
+
|
|
283
|
+
if self.is_external:
|
|
284
|
+
if not self.cluster_name:
|
|
285
|
+
raise ValueError(
|
|
286
|
+
f"{self.__class__.__name__}: 'cluster_name' is required when is_external=True"
|
|
287
|
+
)
|
|
288
|
+
db = ExternalMongoManager.get_database(self.cluster_name, db_name)
|
|
289
|
+
else:
|
|
290
|
+
db = MongoManager.get_database(db_name)
|
|
291
|
+
collection = db[self.collection_name]
|
|
292
|
+
|
|
293
|
+
declared = {
|
|
294
|
+
tuple(tuple(field) for field in index_def["key"])
|
|
295
|
+
for index_def in self.indexes
|
|
296
|
+
if index_def.get("key")
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
orphans: List[str] = []
|
|
300
|
+
async for existing in collection.list_indexes():
|
|
301
|
+
name = existing.get("name")
|
|
302
|
+
if name == "_id_": # the default _id index can never be dropped
|
|
303
|
+
continue
|
|
304
|
+
|
|
305
|
+
key_spec = list(existing.get("key", {}).items())
|
|
306
|
+
# Skip special indexes (text/geo/hashed): MongoDB rewrites their key
|
|
307
|
+
# spec ("text"/2dsphere/"hashed", _fts/_ftsx), so a key-based
|
|
308
|
+
# comparison would wrongly flag them as orphans.
|
|
309
|
+
if any(isinstance(direction, str) for _, direction in key_spec) \
|
|
310
|
+
or any(field in ("_fts", "_ftsx") for field, _ in key_spec):
|
|
311
|
+
continue
|
|
312
|
+
|
|
313
|
+
if tuple(key_spec) not in declared:
|
|
314
|
+
orphans.append(name)
|
|
315
|
+
if not dry_run:
|
|
316
|
+
await collection.drop_index(name)
|
|
317
|
+
self.logger.info(
|
|
318
|
+
f"Dropped orphan index '{name}' on '{db_name}.{self.collection_name}'"
|
|
319
|
+
)
|
|
320
|
+
else:
|
|
321
|
+
self.logger.info(
|
|
322
|
+
f"[dry-run] orphan index '{name}' on '{db_name}.{self.collection_name}'"
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
return orphans
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aws-python-helper
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.3
|
|
4
4
|
Summary: AWS Python Helper Framework
|
|
5
5
|
Author-email: Fabian Claros <neufabiae@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -39,7 +39,7 @@ Mini-framework to create REST APIs, SQS Consumers, SNS Publishers, Fargate Tasks
|
|
|
39
39
|
- **LambdaInvoker**: Invoke other Lambda functions (sync or async) with built-in error handling
|
|
40
40
|
- **ApiClient**: HTTP client for inter-service communication — resolves service URLs and auth token from environment variables automatically
|
|
41
41
|
- **ModelQueryLambda**: Base class for MongoDB query-proxy Lambdas — declare an `allowed_collections` whitelist and the framework handles `find` and `aggregate` operations generically
|
|
42
|
-
- **ModelIndexSyncLambda**: Base class for index-synchronization Lambdas — declare your `repositories` and the framework creates
|
|
42
|
+
- **ModelIndexSyncLambda**: Base class for index-synchronization Lambdas — declare your `repositories` and the framework creates the declared indexes **and prunes orphans** across the `core` database and all active state databases, idempotently and in parallel, in a single run (run once per deploy)
|
|
43
43
|
- **JSON utilities**: Automatic serialization of MongoDB types
|
|
44
44
|
- **Type hints**: Modern Python with type annotations
|
|
45
45
|
- **Async/await**: Full support for asynchronous operations
|
|
@@ -68,7 +68,7 @@ All available classes and functions:
|
|
|
68
68
|
| `fargate_handler` | `aws_python_helper.fargate.handler` | Entry point handler for Fargate |
|
|
69
69
|
| `Repository` | `aws_python_helper.repository.base` | Base class for MongoDB repositories |
|
|
70
70
|
| `ModelQueryLambda` | `aws_python_helper` | Generic MongoDB query-proxy Lambda base class |
|
|
71
|
-
| `ModelIndexSyncLambda` | `aws_python_helper` | Index-sync Lambda base class (creates indexes across `core` + all state DBs) |
|
|
71
|
+
| `ModelIndexSyncLambda` | `aws_python_helper` | Index-sync Lambda base class (creates declared indexes + prunes orphans across `core` + all state DBs, in parallel) |
|
|
72
72
|
| `LambdaInvoker` | `aws_python_helper` | Invoke Lambda functions (sync/async) with error handling |
|
|
73
73
|
| `ApiClient` | `aws_python_helper` | HTTP client for inter-service communication |
|
|
74
74
|
| `LambdaInvocationError` | `aws_python_helper` | Raised when boto3 fails to invoke a Lambda |
|
|
@@ -1360,10 +1360,16 @@ Always pass `self.session.to_dict()` when calling from an API or Lambda to ensur
|
|
|
1360
1360
|
|
|
1361
1361
|
## 🧱 ModelIndexSyncLambda
|
|
1362
1362
|
|
|
1363
|
-
`ModelIndexSyncLambda` is a ready-to-use base class for **
|
|
1363
|
+
`ModelIndexSyncLambda` is a ready-to-use base class for **synchronizing all of a service's MongoDB indexes deterministically**, in one run. Because index creation is no longer triggered at request time (see the [Repository Pattern](#️-repository-pattern) note), this Lambda is the single source of truth for materializing indexes — typically invoked **once per deploy**.
|
|
1364
1364
|
|
|
1365
1365
|
**Why:** in short-lived Lambda/API runtimes, creating indexes lazily on first access often never finishes (the function responds and freezes). This Lambda creates every declared index up front, `await`-ing each one, idempotently — and across **every** active state database, not just the one of the current request.
|
|
1366
1366
|
|
|
1367
|
+
It does a **two-way sync** per collection: it creates the declared indexes **and prunes orphans** — any index present in MongoDB that is no longer declared in the repository's `indexes` is dropped (the default `_id_` index and special text/geo/hashed indexes are always preserved).
|
|
1368
|
+
|
|
1369
|
+
> ⚠️ **Orphan pruning drops indexes not declared in code.** Indexes created out-of-band (by a DBA, Atlas, or another tool) that aren't in any repository's `indexes` will be removed on every run. Declare every index you want to keep.
|
|
1370
|
+
|
|
1371
|
+
**Concurrency & fault tolerance:** each `(collection, database)` unit is synced **in parallel** (bounded by `max_concurrency`, default 10); index work *within* a unit stays sequential. A failure in one unit is **captured and isolated** — it never aborts the others, so a single broken collection doesn't stop the rest of the sync. The top-level response is always `success: true`; per-unit outcomes are reported in `results[].success`.
|
|
1372
|
+
|
|
1367
1373
|
### Creating an index-sync Lambda
|
|
1368
1374
|
|
|
1369
1375
|
Create `src/lambda/model-index-sync/main.py` and declare which repositories to sync:
|
|
@@ -1409,14 +1415,20 @@ This Lambda sets `requires_state = False`: it is not tied to a single state and
|
|
|
1409
1415
|
|
|
1410
1416
|
### Response format
|
|
1411
1417
|
|
|
1418
|
+
The top-level `success` is **always `true`** (a failing collection no longer aborts the run). Inspect `data.summary` for the OK/failed counts and `data.results[].success` for each unit; failed units include an `error` field.
|
|
1419
|
+
|
|
1412
1420
|
```json
|
|
1413
1421
|
{
|
|
1414
1422
|
"success": true,
|
|
1415
|
-
"data":
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
]
|
|
1423
|
+
"data": {
|
|
1424
|
+
"summary": { "total": 4, "succeeded": 3, "failed": 1 },
|
|
1425
|
+
"results": [
|
|
1426
|
+
{"repository": "ParcelsRepository", "database": "connecticut", "collection": "parcels", "indexes": ["parcel_id_1", "..."], "orphan_indexes_dropped": ["old_field_1"], "success": true},
|
|
1427
|
+
{"repository": "ParcelsRepository", "database": "new_jersey", "collection": "parcels", "indexes": ["parcel_id_1", "..."], "orphan_indexes_dropped": [], "success": true},
|
|
1428
|
+
{"repository": "UsersRepository", "database": "core", "collection": "users", "indexes": ["email_1"], "orphan_indexes_dropped": [], "success": true},
|
|
1429
|
+
{"repository": "TokensRepository", "database": "core", "collection": "tokens", "indexes": [], "orphan_indexes_dropped": [], "success": false, "error": "..."}
|
|
1430
|
+
]
|
|
1431
|
+
}
|
|
1420
1432
|
}
|
|
1421
1433
|
```
|
|
1422
1434
|
|
|
@@ -1427,11 +1439,14 @@ This Lambda sets `requires_state = False`: it is not tied to a single state and
|
|
|
1427
1439
|
| `repositories` | `[]` | **Required.** List of `Repository` subclasses (the classes, not instances) to sync. Empty raises a validation error |
|
|
1428
1440
|
| `states_database` | `"core"` | Database holding the states collection |
|
|
1429
1441
|
| `states_collection` | `"states"` | Collection listing the states (filtered by `is_active = true`, field `name`) |
|
|
1442
|
+
| `max_concurrency` | `10` | Max `(collection, database)` units synced in parallel. `0` (or negative) = unlimited |
|
|
1430
1443
|
| `requires_state` | `False` | Inherited override — the Lambda runs across all states, no session required |
|
|
1431
1444
|
|
|
1432
1445
|
### Running it once per deploy (Terraform)
|
|
1433
1446
|
|
|
1434
|
-
Since deploys are applied manually with Terraform, attach an `aws_lambda_invocation` that runs **on every `apply`**, right after the function code is updated. `timestamp()` changes on every plan, so it always re-runs (creation is idempotent and also picks up any newly-activated state).
|
|
1447
|
+
Since deploys are applied manually with Terraform, attach an `aws_lambda_invocation` that runs **on every `apply`**, right after the function code is updated. `timestamp()` changes on every plan, so it always re-runs (creation is idempotent and also picks up any newly-activated state).
|
|
1448
|
+
|
|
1449
|
+
> **Detecting failures in the deploy.** The top-level `success` is now **always `true`** — a single failed collection no longer aborts the run, so checking `success` alone won't catch partial failures. If you want the `apply` to fail when any collection failed, point the `postcondition` at `data.summary.failed` instead (shown below). If you'd rather the deploy never fail on index errors and only review them afterward (in the response/logs), simply omit the `lifecycle` block.
|
|
1435
1450
|
|
|
1436
1451
|
```hcl
|
|
1437
1452
|
module "lambda" {
|
|
@@ -1449,10 +1464,13 @@ resource "aws_lambda_invocation" "sync_on_deploy" {
|
|
|
1449
1464
|
|
|
1450
1465
|
depends_on = [module.lambda] # run after the code is updated
|
|
1451
1466
|
|
|
1467
|
+
# Optional: fail the apply if any collection failed to sync.
|
|
1468
|
+
# Omit this whole lifecycle block if you prefer the deploy to never fail on
|
|
1469
|
+
# index errors (review data.results[].success / logs instead).
|
|
1452
1470
|
lifecycle {
|
|
1453
1471
|
postcondition {
|
|
1454
|
-
condition = try(jsondecode(self.result).
|
|
1455
|
-
error_message = "Index sync
|
|
1472
|
+
condition = try(jsondecode(self.result).data.summary.failed, 1) == 0
|
|
1473
|
+
error_message = "Index sync had failures during apply: ${self.result}"
|
|
1456
1474
|
}
|
|
1457
1475
|
}
|
|
1458
1476
|
}
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
ModelIndexSyncLambda - Synchronizes MongoDB indexes for a service's repositories.
|
|
3
|
-
|
|
4
|
-
Creates (idempotently) every index declared in the service's repositories, in
|
|
5
|
-
the correct databases:
|
|
6
|
-
- Repositories with a fixed database_key (e.g. 'core') → that database.
|
|
7
|
-
- State-scoped repositories (database_key is None) → every active state
|
|
8
|
-
database, read from core.states (is_active=True). A state-scoped collection
|
|
9
|
-
(e.g. 'parcels') therefore gets its indexes created in 'connecticut',
|
|
10
|
-
'new_jersey', etc.
|
|
11
|
-
|
|
12
|
-
Designed to run once per deploy (and on demand), guaranteeing index creation
|
|
13
|
-
without the per-request overhead of creating indexes at runtime.
|
|
14
|
-
|
|
15
|
-
Usage — subclass and declare the repositories to sync:
|
|
16
|
-
|
|
17
|
-
import aws_python_helper
|
|
18
|
-
from repositories.parcels import ParcelsRepository
|
|
19
|
-
from repositories.users import UsersRepository
|
|
20
|
-
|
|
21
|
-
class PropertiesModelIndexSyncLambda(aws_python_helper.ModelIndexSyncLambda):
|
|
22
|
-
@property
|
|
23
|
-
def repositories(self) -> list:
|
|
24
|
-
return [ParcelsRepository, UsersRepository]
|
|
25
|
-
|
|
26
|
-
This Lambda does not require a session state (requires_state = False); it
|
|
27
|
-
operates across all active states.
|
|
28
|
-
|
|
29
|
-
Response (via Lambda base run()):
|
|
30
|
-
{"success": True, "data": [
|
|
31
|
-
{"repository": "ParcelsRepository", "database": "connecticut",
|
|
32
|
-
"collection": "parcels", "indexes": ["parcel_id_1", ...]},
|
|
33
|
-
{"repository": "UsersRepository", "database": "core",
|
|
34
|
-
"collection": "users", "indexes": ["email_1"]},
|
|
35
|
-
...
|
|
36
|
-
]}
|
|
37
|
-
"""
|
|
38
|
-
|
|
39
|
-
from typing import Any, List, Type
|
|
40
|
-
|
|
41
|
-
from ..lambda_standalone.base import Lambda
|
|
42
|
-
from ..repository.base import Repository
|
|
43
|
-
from ..database.mongo_manager import MongoManager
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
class ModelIndexSyncLambda(Lambda):
|
|
47
|
-
"""
|
|
48
|
-
Base class for index-synchronization Lambdas.
|
|
49
|
-
|
|
50
|
-
Subclass and override `repositories` to declare which repositories to sync.
|
|
51
|
-
Everything else (resolving core vs state-scoped, reading active states and
|
|
52
|
-
creating the indexes) is handled automatically.
|
|
53
|
-
"""
|
|
54
|
-
|
|
55
|
-
@property
|
|
56
|
-
def requires_state(self) -> bool:
|
|
57
|
-
# Operates across all states; not tied to a single one.
|
|
58
|
-
return False
|
|
59
|
-
|
|
60
|
-
@property
|
|
61
|
-
def repositories(self) -> List[Type[Repository]]:
|
|
62
|
-
"""
|
|
63
|
-
Repositories whose indexes should be synchronized.
|
|
64
|
-
|
|
65
|
-
Override returning a list of Repository subclasses (the classes, not
|
|
66
|
-
instances). Declare them explicitly rather than auto-discovering, so the
|
|
67
|
-
synced set is intentional and nothing is silently missed or added.
|
|
68
|
-
"""
|
|
69
|
-
return []
|
|
70
|
-
|
|
71
|
-
@property
|
|
72
|
-
def states_database(self) -> str:
|
|
73
|
-
"""Database holding the states collection. Default: 'core'."""
|
|
74
|
-
return "core"
|
|
75
|
-
|
|
76
|
-
@property
|
|
77
|
-
def states_collection(self) -> str:
|
|
78
|
-
"""Collection listing the states. Default: 'states'."""
|
|
79
|
-
return "states"
|
|
80
|
-
|
|
81
|
-
async def validate(self):
|
|
82
|
-
if not self.repositories:
|
|
83
|
-
raise ValueError(
|
|
84
|
-
"repositories is not configured on this ModelIndexSyncLambda"
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
async def _active_states(self) -> List[str]:
|
|
88
|
-
"""Names of active states — i.e. the state-scoped database names."""
|
|
89
|
-
db = MongoManager.get_database(self.states_database)
|
|
90
|
-
cursor = db[self.states_collection].find(
|
|
91
|
-
{"is_active": True}, {"name": 1, "_id": 0}
|
|
92
|
-
)
|
|
93
|
-
return [doc["name"] async for doc in cursor if doc.get("name")]
|
|
94
|
-
|
|
95
|
-
async def process(self) -> Any:
|
|
96
|
-
active_states = await self._active_states()
|
|
97
|
-
report: List[dict] = []
|
|
98
|
-
|
|
99
|
-
for repo_cls in self.repositories:
|
|
100
|
-
repo = repo_cls()
|
|
101
|
-
# database_key is None -> state-scoped (collection repeats per state db).
|
|
102
|
-
is_state_scoped = repo.database_key is None and not repo.is_external
|
|
103
|
-
target_databases = active_states if is_state_scoped else [repo.database_name]
|
|
104
|
-
|
|
105
|
-
for db_name in target_databases:
|
|
106
|
-
created = await repo.ensure_indexes(database_name=db_name)
|
|
107
|
-
report.append({
|
|
108
|
-
"repository": repo_cls.__name__,
|
|
109
|
-
"database": db_name,
|
|
110
|
-
"collection": repo.collection_name,
|
|
111
|
-
"indexes": created,
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
return report
|
|
File without changes
|
|
File without changes
|
{aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/api/auth_middleware.py
RENAMED
|
File without changes
|
{aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/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
|
|
File without changes
|
|
File without changes
|
{aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/context/state_validator.py
RENAMED
|
File without changes
|
|
File without changes
|
{aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/database/database_proxy.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/database/mongo_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/invoker/lambda_invoker.py
RENAMED
|
File without changes
|
{aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/lambda_standalone/__init__.py
RENAMED
|
File without changes
|
{aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/lambda_standalone/base.py
RENAMED
|
File without changes
|
{aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/lambda_standalone/fetcher.py
RENAMED
|
File without changes
|
{aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/lambda_standalone/handler.py
RENAMED
|
File without changes
|
{aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/model_index_sync/__init__.py
RENAMED
|
File without changes
|
{aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/model_query/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper/repository/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
{aws_python_helper-1.0.2 → aws_python_helper-1.0.3}/aws_python_helper.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|