pygrestqlambda 0.0.2__tar.gz → 0.0.4__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.
- {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/.github/workflows/publish.yml +7 -0
- pygrestqlambda-0.0.4/.gitignore +10 -0
- pygrestqlambda-0.0.4/PKG-INFO +124 -0
- pygrestqlambda-0.0.4/README.md +97 -0
- {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/pyproject.toml +5 -1
- pygrestqlambda-0.0.4/scripts/pre-publish.sh +7 -0
- {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/scripts/validate.sh +4 -1
- pygrestqlambda-0.0.4/src/pygrestqlambda/aws/lambda_function/json_transform.py +29 -0
- pygrestqlambda-0.0.4/src/pygrestqlambda/aws/lambda_function/rest_api_gateway_proxy_integration.py +149 -0
- pygrestqlambda-0.0.4/tests/integration/conftest.py +39 -0
- pygrestqlambda-0.0.4/tests/integration/test_database.py +13 -0
- pygrestqlambda-0.0.4/tests/integration/test_lambda_function.py +14 -0
- pygrestqlambda-0.0.4/tests/integration/utils/constants.py +15 -0
- pygrestqlambda-0.0.4/tests/integration/utils/db_postgres.py +85 -0
- pygrestqlambda-0.0.4/tests/integration/utils/docker.py +49 -0
- pygrestqlambda-0.0.4/tests/integration/utils/lambda_function.py +78 -0
- pygrestqlambda-0.0.4/tests/integration/utils/lambda_function_docker/Dockerfile +9 -0
- pygrestqlambda-0.0.4/tests/integration/utils/lambda_function_docker/__init__.py +0 -0
- pygrestqlambda-0.0.4/tests/integration/utils/lambda_function_docker/app.py +20 -0
- pygrestqlambda-0.0.4/tests/unit/__init__.py +0 -0
- pygrestqlambda-0.0.4/tests/unit/aws/__init__.py +0 -0
- pygrestqlambda-0.0.4/tests/unit/aws/lambda_function/__init__.py +0 -0
- pygrestqlambda-0.0.4/tests/unit/aws/lambda_function/fixtures/base64-request.json +86 -0
- pygrestqlambda-0.0.4/tests/unit/aws/lambda_function/fixtures/non-base64-request.json +86 -0
- pygrestqlambda-0.0.4/tests/unit/aws/lambda_function/test_json_transform.py +46 -0
- {pygrestqlambda-0.0.2/tests → pygrestqlambda-0.0.4/tests/unit}/aws/lambda_function/test_rest_api_gateway_proxy_integration.py +22 -0
- pygrestqlambda-0.0.4/tests/unit/aws/lambda_function/test_rest_api_gateway_proxy_integration_request.py +120 -0
- pygrestqlambda-0.0.2/PKG-INFO +0 -37
- pygrestqlambda-0.0.2/README.md +0 -14
- pygrestqlambda-0.0.2/src/pygrestqlambda/aws/lambda_function/json_transform.py +0 -22
- pygrestqlambda-0.0.2/src/pygrestqlambda/aws/lambda_function/rest_api_gateway_proxy_integration.py +0 -54
- pygrestqlambda-0.0.2/tests/aws/lambda_function/test_json_transform.py +0 -27
- /pygrestqlambda-0.0.2/.gitignore → /pygrestqlambda-0.0.4/.dockerignore +0 -0
- {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/.github/workflows/validate.yml +0 -0
- {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/LICENSE +0 -0
- {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/docs/examples/direct/README.md +0 -0
- {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/docs/examples/direct/json_response.py +0 -0
- {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/docs/examples/direct/requirements.txt +0 -0
- {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/docs/examples/direct/string_response.py +0 -0
- {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/docs/examples/lambda/Dockerfile +0 -0
- {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/docs/examples/lambda/README.md +0 -0
- {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/docs/examples/lambda/app.py +0 -0
- {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/docs/examples/local_lambda_gateway_compose/README.md +0 -0
- {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/docs/examples/local_lambda_gateway_compose/docker-compose.yaml +0 -0
- {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/docs/examples/local_lambda_gateway_compose/lambda/Dockerfile +0 -0
- {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/docs/examples/local_lambda_gateway_compose/lambda/app.py +0 -0
- {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/docs/examples/local_lambda_gateway_compose/lambda/requirements.txt +0 -0
- {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/docs/examples/local_lambda_gateway_compose/openapi.yaml +0 -0
- {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/src/__init__.py +0 -0
- {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/src/pygrestqlambda/__init__.py +0 -0
- {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/src/pygrestqlambda/aws/__init__.py +0 -0
- {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/src/pygrestqlambda/aws/lambda_function/__init__.py +0 -0
- {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/src/pygrestqlambda/db/__init__.py +0 -0
- {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/src/pygrestqlambda/db/record.py +0 -0
- {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/tests/__init__.py +0 -0
- {pygrestqlambda-0.0.2/tests/aws → pygrestqlambda-0.0.4/tests/integration}/__init__.py +0 -0
- {pygrestqlambda-0.0.2/tests/aws/lambda_function → pygrestqlambda-0.0.4/tests/integration/utils}/__init__.py +0 -0
- {pygrestqlambda-0.0.2/tests → pygrestqlambda-0.0.4/tests/unit}/test_record.py +0 -0
@@ -4,6 +4,9 @@ on:
|
|
4
4
|
push:
|
5
5
|
tags: [ '*.*.*' ]
|
6
6
|
|
7
|
+
env:
|
8
|
+
PROJECT_VERSION: ${{ github.ref_name }}
|
9
|
+
|
7
10
|
jobs:
|
8
11
|
publish:
|
9
12
|
runs-on: ubuntu-latest
|
@@ -19,6 +22,10 @@ jobs:
|
|
19
22
|
with:
|
20
23
|
python-version: '3.11'
|
21
24
|
|
25
|
+
# Run pre-publish step to update project version number to tag
|
26
|
+
- name: Pre-build
|
27
|
+
run: ./scripts/pre-publish.sh
|
28
|
+
|
22
29
|
# Re-run validation and build
|
23
30
|
- name: Validate
|
24
31
|
run: ./scripts/validate.sh
|
@@ -0,0 +1,124 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: pygrestqlambda
|
3
|
+
Version: 0.0.4
|
4
|
+
Summary: PostgreSQL REST API framework for AWS Lambda functions
|
5
|
+
Project-URL: Homepage, https://github.com/mesogate/pygrestqlambda
|
6
|
+
Project-URL: Issues, https://github.com/mesogate/pygrestqlambda/issues
|
7
|
+
Author-email: Voquis Limited <opensource@voquis.com>
|
8
|
+
License-File: LICENSE
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
10
|
+
Classifier: Operating System :: OS Independent
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
12
|
+
Requires-Python: >=3.11
|
13
|
+
Requires-Dist: aws-xray-sdk
|
14
|
+
Requires-Dist: boto3
|
15
|
+
Provides-Extra: dev
|
16
|
+
Requires-Dist: build; extra == 'dev'
|
17
|
+
Requires-Dist: docker; extra == 'dev'
|
18
|
+
Requires-Dist: httpx; extra == 'dev'
|
19
|
+
Requires-Dist: psycopg[binary]; extra == 'dev'
|
20
|
+
Requires-Dist: pylint; extra == 'dev'
|
21
|
+
Requires-Dist: pytest; extra == 'dev'
|
22
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
23
|
+
Requires-Dist: pytest-xdist; extra == 'dev'
|
24
|
+
Requires-Dist: ruff; extra == 'dev'
|
25
|
+
Requires-Dist: twine; extra == 'dev'
|
26
|
+
Description-Content-Type: text/markdown
|
27
|
+
|
28
|
+
# Python PostgreSQL REST API framework for AWS Lambda functions
|
29
|
+
> [!NOTE]
|
30
|
+
> Project status: `Alpha`
|
31
|
+
|
32
|
+
A REST API web framework for persisting records in a PostgreSQL database.
|
33
|
+
|
34
|
+
## Supported features
|
35
|
+
- Automatic creation of `uid` fields
|
36
|
+
- Automatic setting of `created_at` and `last_updated_at` timestamps
|
37
|
+
- Automatic setting of `creator_uid` and `last_updater_uid`
|
38
|
+
- RDS with IAM credentials
|
39
|
+
|
40
|
+
## Examples
|
41
|
+
See [Examples docs directory](./docs/examples/)
|
42
|
+
|
43
|
+
## Sequence diagrams
|
44
|
+
|
45
|
+
### High-level infrastructure
|
46
|
+
|
47
|
+
This sequence diagram shows how a lambda function running this library is intended to be deployed.
|
48
|
+
|
49
|
+
```mermaid
|
50
|
+
sequenceDiagram
|
51
|
+
# Set up actors and participants
|
52
|
+
actor User
|
53
|
+
participant APIGW as API Gateway
|
54
|
+
participant Cognito as Cognito
|
55
|
+
box Purple This library
|
56
|
+
participant Lambda as Lambda Function
|
57
|
+
end
|
58
|
+
participant RDS as RDS Database
|
59
|
+
|
60
|
+
# Set up Sequences
|
61
|
+
User ->> APIGW: HTTP /resource
|
62
|
+
activate APIGW
|
63
|
+
APIGW -->> Cognito: Authenticate
|
64
|
+
activate Cognito
|
65
|
+
Cognito ->> APIGW: Authenticated
|
66
|
+
deactivate Cognito
|
67
|
+
APIGW ->> Lambda: Send proxy integration request
|
68
|
+
activate Lambda
|
69
|
+
Lambda ->> RDS: Fetch/mutate
|
70
|
+
activate RDS
|
71
|
+
RDS -->> Lambda: Return records
|
72
|
+
deactivate RDS
|
73
|
+
Lambda -->> APIGW: Return response
|
74
|
+
deactivate Lambda
|
75
|
+
APIGW -->> User: Return response
|
76
|
+
deactivate APIGW
|
77
|
+
```
|
78
|
+
|
79
|
+
### Low-level architecture
|
80
|
+
|
81
|
+
This sequence diagram shows the layers within the library that handle request and response processing.
|
82
|
+
|
83
|
+
```mermaid
|
84
|
+
sequenceDiagram
|
85
|
+
|
86
|
+
Participant APIGW as API Gateway
|
87
|
+
box Purple This library as a deployed Lambda Function
|
88
|
+
Participant CONT as Controller
|
89
|
+
Participant REQM as Request Mapper
|
90
|
+
Participant RESOURCEM as Resource Mapper
|
91
|
+
Participant DBM as Database Mapper
|
92
|
+
Participant RESPONSEM as Response Mapper
|
93
|
+
end
|
94
|
+
Participant RDS
|
95
|
+
|
96
|
+
APIGW ->> CONT: Send `event` dict
|
97
|
+
activate CONT
|
98
|
+
CONT ->> REQM: Map request
|
99
|
+
activate REQM
|
100
|
+
REQM ->> CONT: Mapped request
|
101
|
+
deactivate REQM
|
102
|
+
|
103
|
+
CONT ->> RESOURCEM: Map resource
|
104
|
+
activate RESOURCEM
|
105
|
+
RESOURCEM -->> CONT: Mapped resource
|
106
|
+
deactivate RESOURCEM
|
107
|
+
|
108
|
+
CONT ->> DBM: Request resource operation
|
109
|
+
activate DBM
|
110
|
+
DBM ->> RDS: Perform resource operation
|
111
|
+
activate RDS
|
112
|
+
RDS -->> DBM: Return resources
|
113
|
+
deactivate RDS
|
114
|
+
DBM -->> CONT: Return resource
|
115
|
+
deactivate DBM
|
116
|
+
|
117
|
+
CONT ->> RESPONSEM: Map response
|
118
|
+
activate RESPONSEM
|
119
|
+
RESPONSEM -->> CONT: Mapped response
|
120
|
+
deactivate RESPONSEM
|
121
|
+
|
122
|
+
CONT -->> APIGW: Return response
|
123
|
+
deactivate CONT
|
124
|
+
```
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# Python PostgreSQL REST API framework for AWS Lambda functions
|
2
|
+
> [!NOTE]
|
3
|
+
> Project status: `Alpha`
|
4
|
+
|
5
|
+
A REST API web framework for persisting records in a PostgreSQL database.
|
6
|
+
|
7
|
+
## Supported features
|
8
|
+
- Automatic creation of `uid` fields
|
9
|
+
- Automatic setting of `created_at` and `last_updated_at` timestamps
|
10
|
+
- Automatic setting of `creator_uid` and `last_updater_uid`
|
11
|
+
- RDS with IAM credentials
|
12
|
+
|
13
|
+
## Examples
|
14
|
+
See [Examples docs directory](./docs/examples/)
|
15
|
+
|
16
|
+
## Sequence diagrams
|
17
|
+
|
18
|
+
### High-level infrastructure
|
19
|
+
|
20
|
+
This sequence diagram shows how a lambda function running this library is intended to be deployed.
|
21
|
+
|
22
|
+
```mermaid
|
23
|
+
sequenceDiagram
|
24
|
+
# Set up actors and participants
|
25
|
+
actor User
|
26
|
+
participant APIGW as API Gateway
|
27
|
+
participant Cognito as Cognito
|
28
|
+
box Purple This library
|
29
|
+
participant Lambda as Lambda Function
|
30
|
+
end
|
31
|
+
participant RDS as RDS Database
|
32
|
+
|
33
|
+
# Set up Sequences
|
34
|
+
User ->> APIGW: HTTP /resource
|
35
|
+
activate APIGW
|
36
|
+
APIGW -->> Cognito: Authenticate
|
37
|
+
activate Cognito
|
38
|
+
Cognito ->> APIGW: Authenticated
|
39
|
+
deactivate Cognito
|
40
|
+
APIGW ->> Lambda: Send proxy integration request
|
41
|
+
activate Lambda
|
42
|
+
Lambda ->> RDS: Fetch/mutate
|
43
|
+
activate RDS
|
44
|
+
RDS -->> Lambda: Return records
|
45
|
+
deactivate RDS
|
46
|
+
Lambda -->> APIGW: Return response
|
47
|
+
deactivate Lambda
|
48
|
+
APIGW -->> User: Return response
|
49
|
+
deactivate APIGW
|
50
|
+
```
|
51
|
+
|
52
|
+
### Low-level architecture
|
53
|
+
|
54
|
+
This sequence diagram shows the layers within the library that handle request and response processing.
|
55
|
+
|
56
|
+
```mermaid
|
57
|
+
sequenceDiagram
|
58
|
+
|
59
|
+
Participant APIGW as API Gateway
|
60
|
+
box Purple This library as a deployed Lambda Function
|
61
|
+
Participant CONT as Controller
|
62
|
+
Participant REQM as Request Mapper
|
63
|
+
Participant RESOURCEM as Resource Mapper
|
64
|
+
Participant DBM as Database Mapper
|
65
|
+
Participant RESPONSEM as Response Mapper
|
66
|
+
end
|
67
|
+
Participant RDS
|
68
|
+
|
69
|
+
APIGW ->> CONT: Send `event` dict
|
70
|
+
activate CONT
|
71
|
+
CONT ->> REQM: Map request
|
72
|
+
activate REQM
|
73
|
+
REQM ->> CONT: Mapped request
|
74
|
+
deactivate REQM
|
75
|
+
|
76
|
+
CONT ->> RESOURCEM: Map resource
|
77
|
+
activate RESOURCEM
|
78
|
+
RESOURCEM -->> CONT: Mapped resource
|
79
|
+
deactivate RESOURCEM
|
80
|
+
|
81
|
+
CONT ->> DBM: Request resource operation
|
82
|
+
activate DBM
|
83
|
+
DBM ->> RDS: Perform resource operation
|
84
|
+
activate RDS
|
85
|
+
RDS -->> DBM: Return resources
|
86
|
+
deactivate RDS
|
87
|
+
DBM -->> CONT: Return resource
|
88
|
+
deactivate DBM
|
89
|
+
|
90
|
+
CONT ->> RESPONSEM: Map response
|
91
|
+
activate RESPONSEM
|
92
|
+
RESPONSEM -->> CONT: Mapped response
|
93
|
+
deactivate RESPONSEM
|
94
|
+
|
95
|
+
CONT -->> APIGW: Return response
|
96
|
+
deactivate CONT
|
97
|
+
```
|
@@ -4,7 +4,7 @@ requires = ["hatchling"]
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "pygrestqlambda"
|
7
|
-
version = "0.0.
|
7
|
+
version = "0.0.4"
|
8
8
|
description = "PostgreSQL REST API framework for AWS Lambda functions"
|
9
9
|
readme = "README.md"
|
10
10
|
requires-python = ">=3.11"
|
@@ -25,9 +25,13 @@ dependencies = [
|
|
25
25
|
[project.optional-dependencies]
|
26
26
|
dev = [
|
27
27
|
"build",
|
28
|
+
"docker",
|
29
|
+
"httpx",
|
30
|
+
"psycopg[binary]",
|
28
31
|
"pylint",
|
29
32
|
"pytest",
|
30
33
|
"pytest-cov",
|
34
|
+
"pytest-xdist",
|
31
35
|
"ruff",
|
32
36
|
"twine"
|
33
37
|
]
|
@@ -0,0 +1,7 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
set -euo pipefail
|
3
|
+
|
4
|
+
# Override pyproject version if environment variable is set
|
5
|
+
PROJECT_VERSION=${PROJECT_VERSION:=0.0.0}
|
6
|
+
echo "Setting project version: ${PROJECT_VERSION}"
|
7
|
+
sed -i -e "s/version.*=.*\".*\"/version = \"${PROJECT_VERSION}\"/" pyproject.toml
|
@@ -20,9 +20,12 @@ pylint src tests
|
|
20
20
|
# Run tests and generate coverage reports
|
21
21
|
echo "Running pytest"
|
22
22
|
pytest --cov="$PACKAGE_NAME" \
|
23
|
+
-n auto \
|
23
24
|
--cov-report term \
|
24
25
|
--cov-report html \
|
25
|
-
--cov-fail-under=100.00
|
26
|
+
--cov-fail-under=100.00 \
|
27
|
+
-o log_cli=true \
|
28
|
+
--log-cli-level=INFO
|
26
29
|
|
27
30
|
# Check the build
|
28
31
|
python -m build
|
@@ -0,0 +1,29 @@
|
|
1
|
+
"""
|
2
|
+
JSON output transformer for non-serialisable values
|
3
|
+
"""
|
4
|
+
|
5
|
+
from datetime import date, datetime
|
6
|
+
from decimal import Decimal
|
7
|
+
from uuid import UUID
|
8
|
+
|
9
|
+
def to_string(value: object) -> str:
|
10
|
+
"""
|
11
|
+
Calculates the string version of an object to return in a JSON response
|
12
|
+
"""
|
13
|
+
|
14
|
+
# Handle UUIDs
|
15
|
+
if isinstance(value, UUID):
|
16
|
+
value = str(value)
|
17
|
+
|
18
|
+
# Handle date/timestamps
|
19
|
+
if isinstance(value, datetime):
|
20
|
+
value = value.isoformat()
|
21
|
+
|
22
|
+
if isinstance(value, date):
|
23
|
+
value = value.isoformat()
|
24
|
+
|
25
|
+
# Handle decimals
|
26
|
+
if isinstance(value, Decimal):
|
27
|
+
value = float(value)
|
28
|
+
|
29
|
+
return value
|
pygrestqlambda-0.0.4/src/pygrestqlambda/aws/lambda_function/rest_api_gateway_proxy_integration.py
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
"""
|
2
|
+
Receives payload in format sent by AWS REST API Gateway
|
3
|
+
https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format
|
4
|
+
|
5
|
+
Returns payload structure expected by REST API Gateway
|
6
|
+
https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-output-format
|
7
|
+
"""
|
8
|
+
|
9
|
+
from base64 import b64encode, b64decode
|
10
|
+
from dataclasses import dataclass
|
11
|
+
import json
|
12
|
+
import logging
|
13
|
+
from pygrestqlambda.aws.lambda_function.json_transform import to_string
|
14
|
+
|
15
|
+
|
16
|
+
@dataclass
|
17
|
+
class Response:
|
18
|
+
"""
|
19
|
+
Lambda function proxy response for REST API Gateway
|
20
|
+
"""
|
21
|
+
is_base64_encoded: bool | None = False
|
22
|
+
status_code: int | None = 401
|
23
|
+
headers: dict | None = None
|
24
|
+
multi_value_headers: dict | None = None
|
25
|
+
body: str | dict | None = None
|
26
|
+
|
27
|
+
def get_payload(self) -> dict:
|
28
|
+
"""
|
29
|
+
Gets payload to send to REST API Gateway
|
30
|
+
"""
|
31
|
+
|
32
|
+
is_json = False
|
33
|
+
if isinstance(self.body, dict):
|
34
|
+
is_json = True
|
35
|
+
|
36
|
+
# Set headers
|
37
|
+
if self.headers is None:
|
38
|
+
self.headers = {}
|
39
|
+
|
40
|
+
if "Content-Type" not in self.headers:
|
41
|
+
logging.debug("No content type header set")
|
42
|
+
if is_json:
|
43
|
+
logging.debug("Using application/json for content-type")
|
44
|
+
self.headers["Content-Type"] = "application/json"
|
45
|
+
else:
|
46
|
+
logging.debug("Using text/plain for content-type")
|
47
|
+
self.headers["Content-Type"] = "text/plain"
|
48
|
+
|
49
|
+
# Calculate body
|
50
|
+
if self.is_base64_encoded:
|
51
|
+
body = b64encode(self.body).decode("utf-8")
|
52
|
+
else:
|
53
|
+
if is_json:
|
54
|
+
logging.debug("Body is a JSON object")
|
55
|
+
body = json.dumps(self.body, default=to_string)
|
56
|
+
else:
|
57
|
+
logging.debug("Body is plain text")
|
58
|
+
body = self.body
|
59
|
+
|
60
|
+
logging.debug("Transforming dataclass dictionary to JSON")
|
61
|
+
data = {
|
62
|
+
"isBase64Encoded": self.is_base64_encoded,
|
63
|
+
"statusCode": self.status_code,
|
64
|
+
"headers": self.headers,
|
65
|
+
"multiValueHeaders": self.multi_value_headers,
|
66
|
+
"body": body,
|
67
|
+
}
|
68
|
+
|
69
|
+
return data
|
70
|
+
|
71
|
+
# pylint: disable=too-many-instance-attributes
|
72
|
+
class Request:
|
73
|
+
"""
|
74
|
+
Lambda function proxy integration request
|
75
|
+
"""
|
76
|
+
|
77
|
+
def __init__(self, event: dict):
|
78
|
+
self.event = event
|
79
|
+
# Extract authorisation information
|
80
|
+
self.cognito_uid = self.get_cognito_uid()
|
81
|
+
# Extract headers needed for body and response
|
82
|
+
self.accept: str = self.get_header('accept')
|
83
|
+
self.content_type: str = self.get_header('content-type')
|
84
|
+
# Extract resource
|
85
|
+
self.resource = event.get('resource')
|
86
|
+
self.method = event.get('httpMethod')
|
87
|
+
# Extract parameters
|
88
|
+
self.query_params = event.get('multiValueQueryStringParameters')
|
89
|
+
self.path_params = event.get('pathParameters')
|
90
|
+
# Extract body
|
91
|
+
self.body = self.get_body()
|
92
|
+
|
93
|
+
|
94
|
+
def get_body(self):
|
95
|
+
"""
|
96
|
+
Returns body from request, decodes from base64 if necessary
|
97
|
+
"""
|
98
|
+
|
99
|
+
body = self.event.get('body')
|
100
|
+
content = body
|
101
|
+
if self.event.get('isBase64Encoded'):
|
102
|
+
if body:
|
103
|
+
content = b64decode(body)
|
104
|
+
|
105
|
+
# Handle no content type
|
106
|
+
if self.content_type is None:
|
107
|
+
return content
|
108
|
+
|
109
|
+
# Handle plain text
|
110
|
+
if self.content_type.lower() == 'text/plain':
|
111
|
+
return str(content)
|
112
|
+
|
113
|
+
# Handle JSON
|
114
|
+
if self.content_type.lower() == 'application/json':
|
115
|
+
return json.loads(content)
|
116
|
+
|
117
|
+
return content
|
118
|
+
|
119
|
+
|
120
|
+
def get_cognito_uid(self):
|
121
|
+
"""
|
122
|
+
Retrieve Cognito UID from supplied claim
|
123
|
+
"""
|
124
|
+
claims = self.event.get('requestContext', {}).get('authorizer', {}).get('claims')
|
125
|
+
|
126
|
+
if claims is None:
|
127
|
+
logging.info('No claims in event request context authoriser')
|
128
|
+
return None
|
129
|
+
|
130
|
+
cognito_uid = claims.get('sub')
|
131
|
+
|
132
|
+
return cognito_uid
|
133
|
+
|
134
|
+
|
135
|
+
def get_header(self, header_name: str):
|
136
|
+
"""
|
137
|
+
Retrieve Accept header
|
138
|
+
"""
|
139
|
+
|
140
|
+
headers = self.event.get('headers')
|
141
|
+
if headers is None:
|
142
|
+
return None
|
143
|
+
|
144
|
+
# Lowercase all the headers
|
145
|
+
headers_lower = {k.lower():v for k,v in headers.items()}
|
146
|
+
|
147
|
+
accept = headers_lower.get(header_name.lower())
|
148
|
+
|
149
|
+
return accept
|
@@ -0,0 +1,39 @@
|
|
1
|
+
"""
|
2
|
+
Pytest configuration file, sets up fixtures to be re-used and set up pre-test
|
3
|
+
configuration.
|
4
|
+
Uses [docker](https://docker-py.readthedocs.io/en/stable/index.html) python
|
5
|
+
client to manage container.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from pytest import fixture
|
9
|
+
from tests.integration.utils.constants import POSTGRES_NAME, LAMBDA_FUNCTION_NAME
|
10
|
+
from tests.integration.utils.db_postgres import get_postgres_connection
|
11
|
+
from tests.integration.utils.lambda_function import get_base_url
|
12
|
+
from tests.integration.utils.docker import remove_container
|
13
|
+
|
14
|
+
@fixture(scope="session")
|
15
|
+
def postgres():
|
16
|
+
"""
|
17
|
+
Return a psycopg database connection
|
18
|
+
"""
|
19
|
+
|
20
|
+
return get_postgres_connection()
|
21
|
+
|
22
|
+
|
23
|
+
@fixture(scope="session")
|
24
|
+
def lambda_function_url():
|
25
|
+
"""
|
26
|
+
Return a lambda function for making requests to
|
27
|
+
"""
|
28
|
+
|
29
|
+
return get_base_url()
|
30
|
+
|
31
|
+
|
32
|
+
def pytest_sessionfinish():
|
33
|
+
"""
|
34
|
+
Runs after all tests have completed
|
35
|
+
Tear down containers.
|
36
|
+
"""
|
37
|
+
|
38
|
+
remove_container(POSTGRES_NAME)
|
39
|
+
remove_container(LAMBDA_FUNCTION_NAME)
|
@@ -0,0 +1,13 @@
|
|
1
|
+
"""
|
2
|
+
Database connection tests
|
3
|
+
"""
|
4
|
+
|
5
|
+
from psycopg.connection import Connection
|
6
|
+
|
7
|
+
def test_postgres(postgres: Connection):
|
8
|
+
"""
|
9
|
+
Test whether a connection to a local postgres Docker container can be established.
|
10
|
+
"""
|
11
|
+
|
12
|
+
result = postgres.execute("select 8 + 4 AS twelve").fetchone()
|
13
|
+
assert result['twelve'] == 12
|
@@ -0,0 +1,14 @@
|
|
1
|
+
"""
|
2
|
+
Database connection tests
|
3
|
+
"""
|
4
|
+
|
5
|
+
import httpx
|
6
|
+
|
7
|
+
def test_lambda_function(lambda_function_url: str):
|
8
|
+
"""
|
9
|
+
Test whether a connection to a built local lambda function can be established
|
10
|
+
"""
|
11
|
+
|
12
|
+
|
13
|
+
response = httpx.post(url=lambda_function_url, json={})
|
14
|
+
assert response.json()['body'] == 'hello world'
|
@@ -0,0 +1,15 @@
|
|
1
|
+
"""
|
2
|
+
Integration testing constants, e.g. database connection parameters used by database server
|
3
|
+
containers and test clients.
|
4
|
+
"""
|
5
|
+
|
6
|
+
from uuid import uuid4
|
7
|
+
|
8
|
+
UID = str(uuid4())
|
9
|
+
POSTGRES_NAME = f'pytest-postgres-{UID}'
|
10
|
+
MYSQL_NAME = f'pytest-mysql-{UID}'
|
11
|
+
LAMBDA_FUNCTION_NAME = f'pytest-lambda-{UID}'
|
12
|
+
|
13
|
+
DB_NAME = 'pytest'
|
14
|
+
USERNAME = 'pytest'
|
15
|
+
PASSWORD = 'pytest'
|
@@ -0,0 +1,85 @@
|
|
1
|
+
"""
|
2
|
+
Postgres database Docker integration testing utilities for set up and tear down
|
3
|
+
"""
|
4
|
+
|
5
|
+
import docker
|
6
|
+
from docker.models.containers import Container
|
7
|
+
from docker.errors import NotFound
|
8
|
+
import psycopg
|
9
|
+
from psycopg.connection import Connection
|
10
|
+
from psycopg.rows import dict_row
|
11
|
+
from tests.integration.utils.constants import POSTGRES_NAME, DB_NAME, USERNAME, PASSWORD
|
12
|
+
from tests.integration.utils.docker import wait_for_healthy_container
|
13
|
+
|
14
|
+
|
15
|
+
def create_postgres_container() -> Container:
|
16
|
+
"""
|
17
|
+
Create a postgres container
|
18
|
+
"""
|
19
|
+
|
20
|
+
# Create postgres container
|
21
|
+
client = docker.from_env()
|
22
|
+
container = client.containers.run(
|
23
|
+
image='postgres:17',
|
24
|
+
name=POSTGRES_NAME,
|
25
|
+
detach=True,
|
26
|
+
ports={'5432/tcp': None},
|
27
|
+
environment={
|
28
|
+
'POSTGRES_DB': DB_NAME,
|
29
|
+
'POSTGRES_USER': USERNAME,
|
30
|
+
'POSTGRES_PASSWORD': PASSWORD,
|
31
|
+
},
|
32
|
+
healthcheck={
|
33
|
+
"test": "psql -h localhost -U $POSTGRES_USER -c 'select 3 + 2;'",
|
34
|
+
"interval": 1000000000, # 1 second in nanoseconds
|
35
|
+
"retries": 20,
|
36
|
+
},
|
37
|
+
command="""
|
38
|
+
-c ssl=on
|
39
|
+
-c ssl_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem
|
40
|
+
-c ssl_key_file=/etc/ssl/private/ssl-cert-snakeoil.key
|
41
|
+
"""
|
42
|
+
)
|
43
|
+
|
44
|
+
return container
|
45
|
+
|
46
|
+
|
47
|
+
def get_postgres_container(create: bool = True) -> Container | None:
|
48
|
+
"""
|
49
|
+
Return the running postgres Docker container, starting one if none exist
|
50
|
+
"""
|
51
|
+
|
52
|
+
client = docker.from_env()
|
53
|
+
|
54
|
+
try:
|
55
|
+
container = client.containers.get(container_id=POSTGRES_NAME)
|
56
|
+
except NotFound:
|
57
|
+
if create:
|
58
|
+
container = create_postgres_container()
|
59
|
+
else:
|
60
|
+
return None
|
61
|
+
|
62
|
+
wait_for_healthy_container(container=container)
|
63
|
+
|
64
|
+
return container
|
65
|
+
|
66
|
+
|
67
|
+
def get_postgres_connection() -> Connection:
|
68
|
+
"""
|
69
|
+
Return a connection to the running postgres container
|
70
|
+
"""
|
71
|
+
|
72
|
+
container = get_postgres_container()
|
73
|
+
|
74
|
+
connection = psycopg.connect(
|
75
|
+
host='127.0.0.1',
|
76
|
+
port=container.ports['5432/tcp'][0]['HostPort'],
|
77
|
+
user=USERNAME,
|
78
|
+
password=PASSWORD,
|
79
|
+
dbname=DB_NAME,
|
80
|
+
sslmode='require',
|
81
|
+
)
|
82
|
+
|
83
|
+
connection.row_factory = dict_row
|
84
|
+
|
85
|
+
return connection
|