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.
Files changed (58) hide show
  1. {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/.github/workflows/publish.yml +7 -0
  2. pygrestqlambda-0.0.4/.gitignore +10 -0
  3. pygrestqlambda-0.0.4/PKG-INFO +124 -0
  4. pygrestqlambda-0.0.4/README.md +97 -0
  5. {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/pyproject.toml +5 -1
  6. pygrestqlambda-0.0.4/scripts/pre-publish.sh +7 -0
  7. {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/scripts/validate.sh +4 -1
  8. pygrestqlambda-0.0.4/src/pygrestqlambda/aws/lambda_function/json_transform.py +29 -0
  9. pygrestqlambda-0.0.4/src/pygrestqlambda/aws/lambda_function/rest_api_gateway_proxy_integration.py +149 -0
  10. pygrestqlambda-0.0.4/tests/integration/conftest.py +39 -0
  11. pygrestqlambda-0.0.4/tests/integration/test_database.py +13 -0
  12. pygrestqlambda-0.0.4/tests/integration/test_lambda_function.py +14 -0
  13. pygrestqlambda-0.0.4/tests/integration/utils/constants.py +15 -0
  14. pygrestqlambda-0.0.4/tests/integration/utils/db_postgres.py +85 -0
  15. pygrestqlambda-0.0.4/tests/integration/utils/docker.py +49 -0
  16. pygrestqlambda-0.0.4/tests/integration/utils/lambda_function.py +78 -0
  17. pygrestqlambda-0.0.4/tests/integration/utils/lambda_function_docker/Dockerfile +9 -0
  18. pygrestqlambda-0.0.4/tests/integration/utils/lambda_function_docker/__init__.py +0 -0
  19. pygrestqlambda-0.0.4/tests/integration/utils/lambda_function_docker/app.py +20 -0
  20. pygrestqlambda-0.0.4/tests/unit/__init__.py +0 -0
  21. pygrestqlambda-0.0.4/tests/unit/aws/__init__.py +0 -0
  22. pygrestqlambda-0.0.4/tests/unit/aws/lambda_function/__init__.py +0 -0
  23. pygrestqlambda-0.0.4/tests/unit/aws/lambda_function/fixtures/base64-request.json +86 -0
  24. pygrestqlambda-0.0.4/tests/unit/aws/lambda_function/fixtures/non-base64-request.json +86 -0
  25. pygrestqlambda-0.0.4/tests/unit/aws/lambda_function/test_json_transform.py +46 -0
  26. {pygrestqlambda-0.0.2/tests → pygrestqlambda-0.0.4/tests/unit}/aws/lambda_function/test_rest_api_gateway_proxy_integration.py +22 -0
  27. pygrestqlambda-0.0.4/tests/unit/aws/lambda_function/test_rest_api_gateway_proxy_integration_request.py +120 -0
  28. pygrestqlambda-0.0.2/PKG-INFO +0 -37
  29. pygrestqlambda-0.0.2/README.md +0 -14
  30. pygrestqlambda-0.0.2/src/pygrestqlambda/aws/lambda_function/json_transform.py +0 -22
  31. pygrestqlambda-0.0.2/src/pygrestqlambda/aws/lambda_function/rest_api_gateway_proxy_integration.py +0 -54
  32. pygrestqlambda-0.0.2/tests/aws/lambda_function/test_json_transform.py +0 -27
  33. /pygrestqlambda-0.0.2/.gitignore → /pygrestqlambda-0.0.4/.dockerignore +0 -0
  34. {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/.github/workflows/validate.yml +0 -0
  35. {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/LICENSE +0 -0
  36. {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/docs/examples/direct/README.md +0 -0
  37. {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/docs/examples/direct/json_response.py +0 -0
  38. {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/docs/examples/direct/requirements.txt +0 -0
  39. {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/docs/examples/direct/string_response.py +0 -0
  40. {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/docs/examples/lambda/Dockerfile +0 -0
  41. {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/docs/examples/lambda/README.md +0 -0
  42. {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/docs/examples/lambda/app.py +0 -0
  43. {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/docs/examples/local_lambda_gateway_compose/README.md +0 -0
  44. {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/docs/examples/local_lambda_gateway_compose/docker-compose.yaml +0 -0
  45. {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/docs/examples/local_lambda_gateway_compose/lambda/Dockerfile +0 -0
  46. {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/docs/examples/local_lambda_gateway_compose/lambda/app.py +0 -0
  47. {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/docs/examples/local_lambda_gateway_compose/lambda/requirements.txt +0 -0
  48. {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/docs/examples/local_lambda_gateway_compose/openapi.yaml +0 -0
  49. {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/src/__init__.py +0 -0
  50. {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/src/pygrestqlambda/__init__.py +0 -0
  51. {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/src/pygrestqlambda/aws/__init__.py +0 -0
  52. {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/src/pygrestqlambda/aws/lambda_function/__init__.py +0 -0
  53. {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/src/pygrestqlambda/db/__init__.py +0 -0
  54. {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/src/pygrestqlambda/db/record.py +0 -0
  55. {pygrestqlambda-0.0.2 → pygrestqlambda-0.0.4}/tests/__init__.py +0 -0
  56. {pygrestqlambda-0.0.2/tests/aws → pygrestqlambda-0.0.4/tests/integration}/__init__.py +0 -0
  57. {pygrestqlambda-0.0.2/tests/aws/lambda_function → pygrestqlambda-0.0.4/tests/integration/utils}/__init__.py +0 -0
  58. {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,10 @@
1
+ # Python
2
+ __pycache__
3
+
4
+ # Pytest
5
+ /,pytest_cache
6
+ /.coverage*
7
+ /htmlcov
8
+
9
+ # Build
10
+ /dist
@@ -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.2"
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
@@ -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