aws-python-helper 1.0.2__tar.gz → 1.1.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-1.0.2/aws_python_helper.egg-info → aws_python_helper-1.1.0}/PKG-INFO +32 -12
- aws_python_helper-1.0.2/PKG-INFO → aws_python_helper-1.1.0/README.md +29 -31
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/api/auth_validators.py +109 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/api/dispatcher.py +5 -5
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/api/fetcher.py +10 -1
- aws_python_helper-1.1.0/aws_python_helper/model_index_sync/base.py +191 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/repository/base.py +68 -0
- aws_python_helper-1.0.2/README.md → aws_python_helper-1.1.0/aws_python_helper.egg-info/PKG-INFO +51 -11
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper.egg-info/requires.txt +3 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/pyproject.toml +5 -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.1.0}/aws_python_helper/__init__.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/api/__init__.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/api/auth_middleware.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/api/base.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/api/exceptions.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/api/handler.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/context/__init__.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/context/session.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/context/state_validator.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/database/__init__.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/database/database_proxy.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/database/external_database_proxy.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/database/external_mongo_manager.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/database/mongo_manager.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/fargate/__init__.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/fargate/executor.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/fargate/fetcher.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/fargate/handler.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/fargate/task_base.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/invoker/__init__.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/invoker/api_client.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/invoker/exceptions.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/invoker/lambda_invoker.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/lambda_standalone/__init__.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/lambda_standalone/base.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/lambda_standalone/fetcher.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/lambda_standalone/handler.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/model_index_sync/__init__.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/model_query/__init__.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/model_query/base.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/repository/__init__.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/sns/__init__.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/sns/publisher.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/sqs/__init__.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/sqs/consumer_base.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/sqs/fetcher.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/sqs/handler.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/utils/__init__.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/utils/json_encoder.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/utils/response.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/utils/serializer.py +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper.egg-info/SOURCES.txt +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper.egg-info/dependency_links.txt +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper.egg-info/top_level.txt +0 -0
- {aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/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.1.0
|
|
4
4
|
Summary: AWS Python Helper Framework
|
|
5
5
|
Author-email: Fabian Claros <neufabiae@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -17,6 +17,8 @@ Requires-Dist: motor==3.3.2
|
|
|
17
17
|
Requires-Dist: pymongo==4.6.1
|
|
18
18
|
Requires-Dist: pydantic>=2.0
|
|
19
19
|
Requires-Dist: httpx>=0.27.0
|
|
20
|
+
Provides-Extra: jwt
|
|
21
|
+
Requires-Dist: PyJWT[crypto]>=2.9.0; extra == "jwt"
|
|
20
22
|
|
|
21
23
|
# AWS Python Framework
|
|
22
24
|
|
|
@@ -39,7 +41,7 @@ Mini-framework to create REST APIs, SQS Consumers, SNS Publishers, Fargate Tasks
|
|
|
39
41
|
- **LambdaInvoker**: Invoke other Lambda functions (sync or async) with built-in error handling
|
|
40
42
|
- **ApiClient**: HTTP client for inter-service communication — resolves service URLs and auth token from environment variables automatically
|
|
41
43
|
- **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
|
|
44
|
+
- **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
45
|
- **JSON utilities**: Automatic serialization of MongoDB types
|
|
44
46
|
- **Type hints**: Modern Python with type annotations
|
|
45
47
|
- **Async/await**: Full support for asynchronous operations
|
|
@@ -68,7 +70,7 @@ All available classes and functions:
|
|
|
68
70
|
| `fargate_handler` | `aws_python_helper.fargate.handler` | Entry point handler for Fargate |
|
|
69
71
|
| `Repository` | `aws_python_helper.repository.base` | Base class for MongoDB repositories |
|
|
70
72
|
| `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) |
|
|
73
|
+
| `ModelIndexSyncLambda` | `aws_python_helper` | Index-sync Lambda base class (creates declared indexes + prunes orphans across `core` + all state DBs, in parallel) |
|
|
72
74
|
| `LambdaInvoker` | `aws_python_helper` | Invoke Lambda functions (sync/async) with error handling |
|
|
73
75
|
| `ApiClient` | `aws_python_helper` | HTTP client for inter-service communication |
|
|
74
76
|
| `LambdaInvocationError` | `aws_python_helper` | Raised when boto3 fails to invoke a Lambda |
|
|
@@ -1360,10 +1362,16 @@ Always pass `self.session.to_dict()` when calling from an API or Lambda to ensur
|
|
|
1360
1362
|
|
|
1361
1363
|
## 🧱 ModelIndexSyncLambda
|
|
1362
1364
|
|
|
1363
|
-
`ModelIndexSyncLambda` is a ready-to-use base class for **
|
|
1365
|
+
`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
1366
|
|
|
1365
1367
|
**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
1368
|
|
|
1369
|
+
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).
|
|
1370
|
+
|
|
1371
|
+
> ⚠️ **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.
|
|
1372
|
+
|
|
1373
|
+
**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`.
|
|
1374
|
+
|
|
1367
1375
|
### Creating an index-sync Lambda
|
|
1368
1376
|
|
|
1369
1377
|
Create `src/lambda/model-index-sync/main.py` and declare which repositories to sync:
|
|
@@ -1409,14 +1417,20 @@ This Lambda sets `requires_state = False`: it is not tied to a single state and
|
|
|
1409
1417
|
|
|
1410
1418
|
### Response format
|
|
1411
1419
|
|
|
1420
|
+
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.
|
|
1421
|
+
|
|
1412
1422
|
```json
|
|
1413
1423
|
{
|
|
1414
1424
|
"success": true,
|
|
1415
|
-
"data":
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
]
|
|
1425
|
+
"data": {
|
|
1426
|
+
"summary": { "total": 4, "succeeded": 3, "failed": 1 },
|
|
1427
|
+
"results": [
|
|
1428
|
+
{"repository": "ParcelsRepository", "database": "connecticut", "collection": "parcels", "indexes": ["parcel_id_1", "..."], "orphan_indexes_dropped": ["old_field_1"], "success": true},
|
|
1429
|
+
{"repository": "ParcelsRepository", "database": "new_jersey", "collection": "parcels", "indexes": ["parcel_id_1", "..."], "orphan_indexes_dropped": [], "success": true},
|
|
1430
|
+
{"repository": "UsersRepository", "database": "core", "collection": "users", "indexes": ["email_1"], "orphan_indexes_dropped": [], "success": true},
|
|
1431
|
+
{"repository": "TokensRepository", "database": "core", "collection": "tokens", "indexes": [], "orphan_indexes_dropped": [], "success": false, "error": "..."}
|
|
1432
|
+
]
|
|
1433
|
+
}
|
|
1420
1434
|
}
|
|
1421
1435
|
```
|
|
1422
1436
|
|
|
@@ -1427,11 +1441,14 @@ This Lambda sets `requires_state = False`: it is not tied to a single state and
|
|
|
1427
1441
|
| `repositories` | `[]` | **Required.** List of `Repository` subclasses (the classes, not instances) to sync. Empty raises a validation error |
|
|
1428
1442
|
| `states_database` | `"core"` | Database holding the states collection |
|
|
1429
1443
|
| `states_collection` | `"states"` | Collection listing the states (filtered by `is_active = true`, field `name`) |
|
|
1444
|
+
| `max_concurrency` | `10` | Max `(collection, database)` units synced in parallel. `0` (or negative) = unlimited |
|
|
1430
1445
|
| `requires_state` | `False` | Inherited override — the Lambda runs across all states, no session required |
|
|
1431
1446
|
|
|
1432
1447
|
### Running it once per deploy (Terraform)
|
|
1433
1448
|
|
|
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).
|
|
1449
|
+
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).
|
|
1450
|
+
|
|
1451
|
+
> **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
1452
|
|
|
1436
1453
|
```hcl
|
|
1437
1454
|
module "lambda" {
|
|
@@ -1449,10 +1466,13 @@ resource "aws_lambda_invocation" "sync_on_deploy" {
|
|
|
1449
1466
|
|
|
1450
1467
|
depends_on = [module.lambda] # run after the code is updated
|
|
1451
1468
|
|
|
1469
|
+
# Optional: fail the apply if any collection failed to sync.
|
|
1470
|
+
# Omit this whole lifecycle block if you prefer the deploy to never fail on
|
|
1471
|
+
# index errors (review data.results[].success / logs instead).
|
|
1452
1472
|
lifecycle {
|
|
1453
1473
|
postcondition {
|
|
1454
|
-
condition = try(jsondecode(self.result).
|
|
1455
|
-
error_message = "Index sync
|
|
1474
|
+
condition = try(jsondecode(self.result).data.summary.failed, 1) == 0
|
|
1475
|
+
error_message = "Index sync had failures during apply: ${self.result}"
|
|
1456
1476
|
}
|
|
1457
1477
|
}
|
|
1458
1478
|
}
|
|
@@ -1,23 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: aws-python-helper
|
|
3
|
-
Version: 1.0.2
|
|
4
|
-
Summary: AWS Python Helper Framework
|
|
5
|
-
Author-email: Fabian Claros <neufabiae@gmail.com>
|
|
6
|
-
License-Expression: MIT
|
|
7
|
-
Project-URL: Homepage, https://github.com/fabiae/aws-python-framework
|
|
8
|
-
Project-URL: Source Code, https://github.com/fabiae/aws-python-framework
|
|
9
|
-
Project-URL: Bug Tracker, https://github.com/fabiae/aws-python-framework/issues
|
|
10
|
-
Project-URL: Documentation, https://github.com/fabiae/aws-python-framework/blob/main/README.md
|
|
11
|
-
Keywords: aws,python,framework,helper,mongodb,sqs,sns,fargate,lambda
|
|
12
|
-
Classifier: Programming Language :: Python :: 3
|
|
13
|
-
Classifier: Operating System :: OS Independent
|
|
14
|
-
Requires-Python: >=3.9
|
|
15
|
-
Description-Content-Type: text/markdown
|
|
16
|
-
Requires-Dist: motor==3.3.2
|
|
17
|
-
Requires-Dist: pymongo==4.6.1
|
|
18
|
-
Requires-Dist: pydantic>=2.0
|
|
19
|
-
Requires-Dist: httpx>=0.27.0
|
|
20
|
-
|
|
21
1
|
# AWS Python Framework
|
|
22
2
|
|
|
23
3
|
Mini-framework to create REST APIs, SQS Consumers, SNS Publishers, Fargate Tasks, and Standalone Lambdas with Python in AWS Lambda.
|
|
@@ -39,7 +19,7 @@ Mini-framework to create REST APIs, SQS Consumers, SNS Publishers, Fargate Tasks
|
|
|
39
19
|
- **LambdaInvoker**: Invoke other Lambda functions (sync or async) with built-in error handling
|
|
40
20
|
- **ApiClient**: HTTP client for inter-service communication — resolves service URLs and auth token from environment variables automatically
|
|
41
21
|
- **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
|
|
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)
|
|
43
23
|
- **JSON utilities**: Automatic serialization of MongoDB types
|
|
44
24
|
- **Type hints**: Modern Python with type annotations
|
|
45
25
|
- **Async/await**: Full support for asynchronous operations
|
|
@@ -68,7 +48,7 @@ All available classes and functions:
|
|
|
68
48
|
| `fargate_handler` | `aws_python_helper.fargate.handler` | Entry point handler for Fargate |
|
|
69
49
|
| `Repository` | `aws_python_helper.repository.base` | Base class for MongoDB repositories |
|
|
70
50
|
| `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) |
|
|
51
|
+
| `ModelIndexSyncLambda` | `aws_python_helper` | Index-sync Lambda base class (creates declared indexes + prunes orphans across `core` + all state DBs, in parallel) |
|
|
72
52
|
| `LambdaInvoker` | `aws_python_helper` | Invoke Lambda functions (sync/async) with error handling |
|
|
73
53
|
| `ApiClient` | `aws_python_helper` | HTTP client for inter-service communication |
|
|
74
54
|
| `LambdaInvocationError` | `aws_python_helper` | Raised when boto3 fails to invoke a Lambda |
|
|
@@ -1360,10 +1340,16 @@ Always pass `self.session.to_dict()` when calling from an API or Lambda to ensur
|
|
|
1360
1340
|
|
|
1361
1341
|
## 🧱 ModelIndexSyncLambda
|
|
1362
1342
|
|
|
1363
|
-
`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**.
|
|
1364
1344
|
|
|
1365
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.
|
|
1366
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
|
+
|
|
1367
1353
|
### Creating an index-sync Lambda
|
|
1368
1354
|
|
|
1369
1355
|
Create `src/lambda/model-index-sync/main.py` and declare which repositories to sync:
|
|
@@ -1409,14 +1395,20 @@ This Lambda sets `requires_state = False`: it is not tied to a single state and
|
|
|
1409
1395
|
|
|
1410
1396
|
### Response format
|
|
1411
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
|
+
|
|
1412
1400
|
```json
|
|
1413
1401
|
{
|
|
1414
1402
|
"success": true,
|
|
1415
|
-
"data":
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
]
|
|
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
|
+
}
|
|
1420
1412
|
}
|
|
1421
1413
|
```
|
|
1422
1414
|
|
|
@@ -1427,11 +1419,14 @@ This Lambda sets `requires_state = False`: it is not tied to a single state and
|
|
|
1427
1419
|
| `repositories` | `[]` | **Required.** List of `Repository` subclasses (the classes, not instances) to sync. Empty raises a validation error |
|
|
1428
1420
|
| `states_database` | `"core"` | Database holding the states collection |
|
|
1429
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 |
|
|
1430
1423
|
| `requires_state` | `False` | Inherited override — the Lambda runs across all states, no session required |
|
|
1431
1424
|
|
|
1432
1425
|
### Running it once per deploy (Terraform)
|
|
1433
1426
|
|
|
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).
|
|
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.
|
|
1435
1430
|
|
|
1436
1431
|
```hcl
|
|
1437
1432
|
module "lambda" {
|
|
@@ -1449,10 +1444,13 @@ resource "aws_lambda_invocation" "sync_on_deploy" {
|
|
|
1449
1444
|
|
|
1450
1445
|
depends_on = [module.lambda] # run after the code is updated
|
|
1451
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).
|
|
1452
1450
|
lifecycle {
|
|
1453
1451
|
postcondition {
|
|
1454
|
-
condition = try(jsondecode(self.result).
|
|
1455
|
-
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}"
|
|
1456
1454
|
}
|
|
1457
1455
|
}
|
|
1458
1456
|
}
|
{aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/api/auth_validators.py
RENAMED
|
@@ -141,3 +141,112 @@ class TokenValidator(AuthValidator):
|
|
|
141
141
|
'token_data': token_doc,
|
|
142
142
|
'is_bypass': False
|
|
143
143
|
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class JWTValidator(AuthValidator):
|
|
147
|
+
"""
|
|
148
|
+
Validates RS256 JWTs issued by constitution-core.
|
|
149
|
+
|
|
150
|
+
Stateless: the signature and the claims are enough, so no database is
|
|
151
|
+
touched. That is what lets any microservice authenticate a request without
|
|
152
|
+
calling the issuer.
|
|
153
|
+
|
|
154
|
+
Environment:
|
|
155
|
+
JWT_PUBLIC_KEY: RSA public key in PEM, raw or base64-encoded.
|
|
156
|
+
JWT_ISSUER: expected `iss`. Defaults to 'constitution-core'.
|
|
157
|
+
JWT_AUDIENCE: expected `aud`. Only verified when set.
|
|
158
|
+
AUTH_BYPASS_TOKEN: still honoured, same as TokenValidator.
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
_public_key_cache: Optional[str] = None
|
|
162
|
+
|
|
163
|
+
@classmethod
|
|
164
|
+
def _public_key(cls) -> str:
|
|
165
|
+
"""The configured public key, decoded once per container."""
|
|
166
|
+
if cls._public_key_cache:
|
|
167
|
+
return cls._public_key_cache
|
|
168
|
+
|
|
169
|
+
raw = os.getenv('JWT_PUBLIC_KEY')
|
|
170
|
+
if not raw:
|
|
171
|
+
raise ValueError(
|
|
172
|
+
"JWT_PUBLIC_KEY environment variable not set. "
|
|
173
|
+
"Required when AUTH_STRATEGY=jwt."
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
key = raw.strip()
|
|
177
|
+
if not key.startswith('-----BEGIN'):
|
|
178
|
+
# PEMs are multi-line, so they travel base64-encoded in env vars.
|
|
179
|
+
import base64
|
|
180
|
+
key = base64.b64decode(key).decode('utf-8')
|
|
181
|
+
|
|
182
|
+
cls._public_key_cache = key
|
|
183
|
+
return key
|
|
184
|
+
|
|
185
|
+
async def validate_token(self, token: str) -> Dict[str, Any]:
|
|
186
|
+
bypass_token = os.getenv('AUTH_BYPASS_TOKEN')
|
|
187
|
+
if bypass_token and token == bypass_token:
|
|
188
|
+
logger.info("Bypass token used - skipping JWT validation")
|
|
189
|
+
return {
|
|
190
|
+
'user_id': 'bypass',
|
|
191
|
+
'user': {
|
|
192
|
+
'email': 'bypass@system',
|
|
193
|
+
'role': 'admin',
|
|
194
|
+
'name': 'Bypass User',
|
|
195
|
+
'_id': 'bypass'
|
|
196
|
+
},
|
|
197
|
+
'is_bypass': True,
|
|
198
|
+
'token_data': None
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
import jwt
|
|
203
|
+
except ImportError as exc:
|
|
204
|
+
raise RuntimeError(
|
|
205
|
+
"PyJWT is required for AUTH_STRATEGY=jwt. Install aws-python-helper[jwt]."
|
|
206
|
+
) from exc
|
|
207
|
+
|
|
208
|
+
audience = os.getenv('JWT_AUDIENCE')
|
|
209
|
+
try:
|
|
210
|
+
claims = jwt.decode(
|
|
211
|
+
token,
|
|
212
|
+
self._public_key(),
|
|
213
|
+
algorithms=['RS256'],
|
|
214
|
+
issuer=os.getenv('JWT_ISSUER', 'constitution-core'),
|
|
215
|
+
audience=audience,
|
|
216
|
+
options={'verify_aud': bool(audience)},
|
|
217
|
+
)
|
|
218
|
+
except jwt.ExpiredSignatureError:
|
|
219
|
+
logger.warning("JWT expired")
|
|
220
|
+
raise UnauthorizedError("Token has expired")
|
|
221
|
+
except jwt.InvalidTokenError as exc:
|
|
222
|
+
logger.warning("JWT rejected: %s", exc)
|
|
223
|
+
raise UnauthorizedError("Invalid token")
|
|
224
|
+
|
|
225
|
+
if not claims.get('sub'):
|
|
226
|
+
raise UnauthorizedError("Invalid token")
|
|
227
|
+
|
|
228
|
+
# Same shape TokenValidator returns, so nothing downstream changes.
|
|
229
|
+
return {
|
|
230
|
+
'user_id': str(claims['sub']),
|
|
231
|
+
'user': {
|
|
232
|
+
'_id': claims['sub'],
|
|
233
|
+
'email': claims.get('email'),
|
|
234
|
+
'name': claims.get('name', ''),
|
|
235
|
+
'role': claims.get('role', 'user'),
|
|
236
|
+
**(claims.get('extra') or {}),
|
|
237
|
+
},
|
|
238
|
+
'token_data': claims,
|
|
239
|
+
'is_bypass': False,
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def get_auth_validator() -> AuthValidator:
|
|
244
|
+
"""The validator this service is configured to use.
|
|
245
|
+
|
|
246
|
+
AUTH_STRATEGY=jwt switches to stateless validation. Anything else keeps the
|
|
247
|
+
database lookup, so a service only migrates when its environment says so.
|
|
248
|
+
"""
|
|
249
|
+
strategy = (os.getenv('AUTH_STRATEGY') or 'db').strip().lower()
|
|
250
|
+
if strategy == 'jwt':
|
|
251
|
+
return JWTValidator()
|
|
252
|
+
return TokenValidator()
|
|
@@ -11,7 +11,7 @@ from .fetcher import Fetcher
|
|
|
11
11
|
from .base import API
|
|
12
12
|
from .exceptions import UnauthorizedError, ForbiddenError, AuthenticationError
|
|
13
13
|
from .auth_middleware import AuthMiddleware
|
|
14
|
-
from .auth_validators import
|
|
14
|
+
from .auth_validators import get_auth_validator
|
|
15
15
|
from ..context.session import get_session
|
|
16
16
|
from ..context.state_validator import StateValidator, InvalidStateError
|
|
17
17
|
|
|
@@ -240,8 +240,8 @@ class Dispatcher:
|
|
|
240
240
|
"""
|
|
241
241
|
Execute authentication middleware
|
|
242
242
|
|
|
243
|
-
|
|
244
|
-
|
|
243
|
+
The validator is chosen by AUTH_STRATEGY: stateless JWT, or the
|
|
244
|
+
database lookup used so far.
|
|
245
245
|
|
|
246
246
|
Args:
|
|
247
247
|
api: API instance to inject authentication data into
|
|
@@ -249,8 +249,8 @@ class Dispatcher:
|
|
|
249
249
|
Raises:
|
|
250
250
|
UnauthorizedError: If authentication fails
|
|
251
251
|
"""
|
|
252
|
-
#
|
|
253
|
-
validator =
|
|
252
|
+
# Strategy comes from configuration, per service
|
|
253
|
+
validator = get_auth_validator()
|
|
254
254
|
|
|
255
255
|
# Create middleware and authenticate
|
|
256
256
|
middleware = AuthMiddleware(validator)
|
|
@@ -91,7 +91,7 @@ class Fetcher:
|
|
|
91
91
|
|
|
92
92
|
def _looks_like_id(self, part: str) -> bool:
|
|
93
93
|
"""
|
|
94
|
-
Check if a URL part looks like an ID (numeric or UUID format)
|
|
94
|
+
Check if a URL part looks like an ID (numeric, ObjectId or UUID format)
|
|
95
95
|
|
|
96
96
|
Args:
|
|
97
97
|
part: URL part to check
|
|
@@ -105,6 +105,15 @@ class Fetcher:
|
|
|
105
105
|
# Check if it's numeric
|
|
106
106
|
if part.isdigit():
|
|
107
107
|
return True
|
|
108
|
+
|
|
109
|
+
# ObjectId de MongoDB: 24 caracteres hexadecimales. Sin esto, un
|
|
110
|
+
# PATCH /users/<oid> busca un directorio con el id como nombre.
|
|
111
|
+
if len(part) == 24:
|
|
112
|
+
try:
|
|
113
|
+
int(part, 16)
|
|
114
|
+
return True
|
|
115
|
+
except ValueError:
|
|
116
|
+
pass
|
|
108
117
|
|
|
109
118
|
# Check if it's a UUID format (8-4-4-4-12 hex digits)
|
|
110
119
|
if len(part) == 36 and part.count('-') == 4:
|
|
@@ -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
|
aws_python_helper-1.0.2/README.md → aws_python_helper-1.1.0/aws_python_helper.egg-info/PKG-INFO
RENAMED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aws-python-helper
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: AWS Python Helper Framework
|
|
5
|
+
Author-email: Fabian Claros <neufabiae@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/fabiae/aws-python-framework
|
|
8
|
+
Project-URL: Source Code, https://github.com/fabiae/aws-python-framework
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/fabiae/aws-python-framework/issues
|
|
10
|
+
Project-URL: Documentation, https://github.com/fabiae/aws-python-framework/blob/main/README.md
|
|
11
|
+
Keywords: aws,python,framework,helper,mongodb,sqs,sns,fargate,lambda
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
Requires-Dist: motor==3.3.2
|
|
17
|
+
Requires-Dist: pymongo==4.6.1
|
|
18
|
+
Requires-Dist: pydantic>=2.0
|
|
19
|
+
Requires-Dist: httpx>=0.27.0
|
|
20
|
+
Provides-Extra: jwt
|
|
21
|
+
Requires-Dist: PyJWT[crypto]>=2.9.0; extra == "jwt"
|
|
22
|
+
|
|
1
23
|
# AWS Python Framework
|
|
2
24
|
|
|
3
25
|
Mini-framework to create REST APIs, SQS Consumers, SNS Publishers, Fargate Tasks, and Standalone Lambdas with Python in AWS Lambda.
|
|
@@ -19,7 +41,7 @@ Mini-framework to create REST APIs, SQS Consumers, SNS Publishers, Fargate Tasks
|
|
|
19
41
|
- **LambdaInvoker**: Invoke other Lambda functions (sync or async) with built-in error handling
|
|
20
42
|
- **ApiClient**: HTTP client for inter-service communication — resolves service URLs and auth token from environment variables automatically
|
|
21
43
|
- **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
|
|
44
|
+
- **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
45
|
- **JSON utilities**: Automatic serialization of MongoDB types
|
|
24
46
|
- **Type hints**: Modern Python with type annotations
|
|
25
47
|
- **Async/await**: Full support for asynchronous operations
|
|
@@ -48,7 +70,7 @@ All available classes and functions:
|
|
|
48
70
|
| `fargate_handler` | `aws_python_helper.fargate.handler` | Entry point handler for Fargate |
|
|
49
71
|
| `Repository` | `aws_python_helper.repository.base` | Base class for MongoDB repositories |
|
|
50
72
|
| `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) |
|
|
73
|
+
| `ModelIndexSyncLambda` | `aws_python_helper` | Index-sync Lambda base class (creates declared indexes + prunes orphans across `core` + all state DBs, in parallel) |
|
|
52
74
|
| `LambdaInvoker` | `aws_python_helper` | Invoke Lambda functions (sync/async) with error handling |
|
|
53
75
|
| `ApiClient` | `aws_python_helper` | HTTP client for inter-service communication |
|
|
54
76
|
| `LambdaInvocationError` | `aws_python_helper` | Raised when boto3 fails to invoke a Lambda |
|
|
@@ -1340,10 +1362,16 @@ Always pass `self.session.to_dict()` when calling from an API or Lambda to ensur
|
|
|
1340
1362
|
|
|
1341
1363
|
## 🧱 ModelIndexSyncLambda
|
|
1342
1364
|
|
|
1343
|
-
`ModelIndexSyncLambda` is a ready-to-use base class for **
|
|
1365
|
+
`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
1366
|
|
|
1345
1367
|
**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
1368
|
|
|
1369
|
+
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).
|
|
1370
|
+
|
|
1371
|
+
> ⚠️ **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.
|
|
1372
|
+
|
|
1373
|
+
**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`.
|
|
1374
|
+
|
|
1347
1375
|
### Creating an index-sync Lambda
|
|
1348
1376
|
|
|
1349
1377
|
Create `src/lambda/model-index-sync/main.py` and declare which repositories to sync:
|
|
@@ -1389,14 +1417,20 @@ This Lambda sets `requires_state = False`: it is not tied to a single state and
|
|
|
1389
1417
|
|
|
1390
1418
|
### Response format
|
|
1391
1419
|
|
|
1420
|
+
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.
|
|
1421
|
+
|
|
1392
1422
|
```json
|
|
1393
1423
|
{
|
|
1394
1424
|
"success": true,
|
|
1395
|
-
"data":
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
]
|
|
1425
|
+
"data": {
|
|
1426
|
+
"summary": { "total": 4, "succeeded": 3, "failed": 1 },
|
|
1427
|
+
"results": [
|
|
1428
|
+
{"repository": "ParcelsRepository", "database": "connecticut", "collection": "parcels", "indexes": ["parcel_id_1", "..."], "orphan_indexes_dropped": ["old_field_1"], "success": true},
|
|
1429
|
+
{"repository": "ParcelsRepository", "database": "new_jersey", "collection": "parcels", "indexes": ["parcel_id_1", "..."], "orphan_indexes_dropped": [], "success": true},
|
|
1430
|
+
{"repository": "UsersRepository", "database": "core", "collection": "users", "indexes": ["email_1"], "orphan_indexes_dropped": [], "success": true},
|
|
1431
|
+
{"repository": "TokensRepository", "database": "core", "collection": "tokens", "indexes": [], "orphan_indexes_dropped": [], "success": false, "error": "..."}
|
|
1432
|
+
]
|
|
1433
|
+
}
|
|
1400
1434
|
}
|
|
1401
1435
|
```
|
|
1402
1436
|
|
|
@@ -1407,11 +1441,14 @@ This Lambda sets `requires_state = False`: it is not tied to a single state and
|
|
|
1407
1441
|
| `repositories` | `[]` | **Required.** List of `Repository` subclasses (the classes, not instances) to sync. Empty raises a validation error |
|
|
1408
1442
|
| `states_database` | `"core"` | Database holding the states collection |
|
|
1409
1443
|
| `states_collection` | `"states"` | Collection listing the states (filtered by `is_active = true`, field `name`) |
|
|
1444
|
+
| `max_concurrency` | `10` | Max `(collection, database)` units synced in parallel. `0` (or negative) = unlimited |
|
|
1410
1445
|
| `requires_state` | `False` | Inherited override — the Lambda runs across all states, no session required |
|
|
1411
1446
|
|
|
1412
1447
|
### Running it once per deploy (Terraform)
|
|
1413
1448
|
|
|
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).
|
|
1449
|
+
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).
|
|
1450
|
+
|
|
1451
|
+
> **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
1452
|
|
|
1416
1453
|
```hcl
|
|
1417
1454
|
module "lambda" {
|
|
@@ -1429,10 +1466,13 @@ resource "aws_lambda_invocation" "sync_on_deploy" {
|
|
|
1429
1466
|
|
|
1430
1467
|
depends_on = [module.lambda] # run after the code is updated
|
|
1431
1468
|
|
|
1469
|
+
# Optional: fail the apply if any collection failed to sync.
|
|
1470
|
+
# Omit this whole lifecycle block if you prefer the deploy to never fail on
|
|
1471
|
+
# index errors (review data.results[].success / logs instead).
|
|
1432
1472
|
lifecycle {
|
|
1433
1473
|
postcondition {
|
|
1434
|
-
condition = try(jsondecode(self.result).
|
|
1435
|
-
error_message = "Index sync
|
|
1474
|
+
condition = try(jsondecode(self.result).data.summary.failed, 1) == 0
|
|
1475
|
+
error_message = "Index sync had failures during apply: ${self.result}"
|
|
1436
1476
|
}
|
|
1437
1477
|
}
|
|
1438
1478
|
}
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "aws-python-helper"
|
|
7
|
-
version = "1.0
|
|
7
|
+
version = "1.1.0"
|
|
8
8
|
description = "AWS Python Helper Framework"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -31,6 +31,10 @@ keywords = [
|
|
|
31
31
|
"aws", "python", "framework", "helper", "mongodb", "sqs", "sns", "fargate", "lambda"
|
|
32
32
|
]
|
|
33
33
|
|
|
34
|
+
# Only needed by services running AUTH_STRATEGY=jwt.
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
jwt = ["PyJWT[crypto]>=2.9.0"]
|
|
37
|
+
|
|
34
38
|
[project.urls]
|
|
35
39
|
"Homepage" = "https://github.com/fabiae/aws-python-framework"
|
|
36
40
|
"Source Code" = "https://github.com/fabiae/aws-python-framework"
|
|
@@ -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.1.0}/aws_python_helper/api/auth_middleware.py
RENAMED
|
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.1.0}/aws_python_helper/context/state_validator.py
RENAMED
|
File without changes
|
|
File without changes
|
{aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/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.1.0}/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.1.0}/aws_python_helper/invoker/lambda_invoker.py
RENAMED
|
File without changes
|
{aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/lambda_standalone/__init__.py
RENAMED
|
File without changes
|
{aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/lambda_standalone/base.py
RENAMED
|
File without changes
|
{aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/lambda_standalone/fetcher.py
RENAMED
|
File without changes
|
{aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/lambda_standalone/handler.py
RENAMED
|
File without changes
|
{aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/model_index_sync/__init__.py
RENAMED
|
File without changes
|
{aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper/model_query/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/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.1.0}/aws_python_helper.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{aws_python_helper-1.0.2 → aws_python_helper-1.1.0}/aws_python_helper.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|