ucam-faas 0.1.2__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.
- ucam_faas-0.1.2/PKG-INFO +194 -0
- ucam_faas-0.1.2/README.md +163 -0
- ucam_faas-0.1.2/pyproject.toml +105 -0
- ucam_faas-0.1.2/ucam_faas/__init__.py +187 -0
- ucam_faas-0.1.2/ucam_faas/testing.py +15 -0
- ucam_faas-0.1.2/ucam_faas/tests/__init__.py +0 -0
- ucam_faas-0.1.2/ucam_faas/tests/test_ucam_faas.py +18 -0
ucam_faas-0.1.2/PKG-INFO
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: ucam-faas
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Opinionated FaaS support framework extending Google's functions-framework
|
|
5
|
+
Home-page: https://gitlab.developers.cam.ac.uk/uis/devops/ucam-faas-python/ucam-faas
|
|
6
|
+
License: MIT
|
|
7
|
+
Author: University of Cambridge Information Services
|
|
8
|
+
Author-email: devops-wilson@uis.cam.ac.uk
|
|
9
|
+
Requires-Python: >=3.9,<4.0
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Provides-Extra: testing
|
|
20
|
+
Requires-Dist: click (>=8.1.7,<9.0.0)
|
|
21
|
+
Requires-Dist: cloudevents (>=1.10.1,<2.0.0)
|
|
22
|
+
Requires-Dist: flask (>=3.0.3,<4.0.0)
|
|
23
|
+
Requires-Dist: functions-framework (>=3.5.0,<4.0.0)
|
|
24
|
+
Requires-Dist: gunicorn (>=22.0.0,<23.0.0)
|
|
25
|
+
Requires-Dist: pytest (>=8.1.1,<9.0.0) ; extra == "testing"
|
|
26
|
+
Requires-Dist: requests (>=2.31.0,<3.0.0)
|
|
27
|
+
Requires-Dist: structlog (>=24.1.0,<25.0.0)
|
|
28
|
+
Project-URL: Repository, https://gitlab.developers.cam.ac.uk/uis/devops/ucam-faas-python/ucam-faas
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# UCam FaaS Library
|
|
32
|
+
|
|
33
|
+
This project contains a support library and base Docker image to be used to
|
|
34
|
+
create Function as a Service (FaaS) applications intended to be deployed to a
|
|
35
|
+
GCP cloud run environment.
|
|
36
|
+
|
|
37
|
+
It is highly opinionated and non-configurable by design.
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
Install the library via pip:
|
|
42
|
+
|
|
43
|
+
```console
|
|
44
|
+
pip install ucam-faas
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Install the library with testing support:
|
|
48
|
+
|
|
49
|
+
```console
|
|
50
|
+
pip install ucam-faas[testing]
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The library provides a decorator to create a runnable application from a single
|
|
54
|
+
function. The function must accept a dictionary as an argument, and return
|
|
55
|
+
either a string or a dictionary as a response:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
# main.py
|
|
59
|
+
from ucam_faas import event_handler
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@event_handler
|
|
63
|
+
def say_hello(event):
|
|
64
|
+
return "hello!"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
This can then be run as a FaaS app using:
|
|
68
|
+
|
|
69
|
+
```console
|
|
70
|
+
ucam-faas --debug --target say_hello
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Testing FaaS Functions
|
|
74
|
+
|
|
75
|
+
To unit test FaaS functions there are two available approaches. Firstly, the
|
|
76
|
+
unwrapped version of the function can be directly accessed. This is recommended
|
|
77
|
+
as the primary way to test FaaS functions and requires no additional
|
|
78
|
+
configuration:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
# test_my_event_handler.py
|
|
82
|
+
from main import say_hello
|
|
83
|
+
|
|
84
|
+
def test_say_hello():
|
|
85
|
+
assert say_hello.__wrapped__({"event_key": "event_value"}) == "hello!"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The original function version is made available through the `__wrapped__`
|
|
89
|
+
variable.
|
|
90
|
+
|
|
91
|
+
Alternatively, if required, a support testing client can be used to instantiate
|
|
92
|
+
a version of the web application running the function. To do this the extra
|
|
93
|
+
"testing" must also be installed:
|
|
94
|
+
|
|
95
|
+
```shell
|
|
96
|
+
pip install ucam-faas[testing]
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Then tests can register the provided `pytest` fixture and use it in tests:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
# test_my_event_handler.py
|
|
103
|
+
pytest_plugins = ["ucam_faas.testing"]
|
|
104
|
+
|
|
105
|
+
def test_say_hello(event_app_client):
|
|
106
|
+
# Provide the target function for the test webapp
|
|
107
|
+
eac = event_app_client("say_hello")
|
|
108
|
+
response = eac.get("/")
|
|
109
|
+
assert response.status_code == 200
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Note that with this approach it is not necessary to import the function under
|
|
113
|
+
test, it is discovered and imported during the test webapp setup.
|
|
114
|
+
|
|
115
|
+
### Example
|
|
116
|
+
|
|
117
|
+
An example application and example tests can be found in this repository in the
|
|
118
|
+
`example` directory.
|
|
119
|
+
|
|
120
|
+
Note that the example dockerfile uses a relative file `FROM` - this means it
|
|
121
|
+
must be built in the context of its parent directory:
|
|
122
|
+
|
|
123
|
+
```console
|
|
124
|
+
docker build -f example/Dockerfile .
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Local Development
|
|
128
|
+
|
|
129
|
+
Install poetry via:
|
|
130
|
+
|
|
131
|
+
```console
|
|
132
|
+
pipx install poetry
|
|
133
|
+
pipx inject poetry poethepoet[poetry_plugin]
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Install dependencies via:
|
|
137
|
+
|
|
138
|
+
```console
|
|
139
|
+
poetry install
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Build the library via:
|
|
143
|
+
|
|
144
|
+
```console
|
|
145
|
+
poetry build
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Run the example application via:
|
|
149
|
+
|
|
150
|
+
```console
|
|
151
|
+
poetry poe example
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Run the tests via:
|
|
155
|
+
|
|
156
|
+
```console
|
|
157
|
+
poetry poe tox
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Note that the tests are found under the example directory, there are *currently*
|
|
161
|
+
no tests in the root library as the code is predominantly configuration and
|
|
162
|
+
setup, and example testing has been deemed sufficient.
|
|
163
|
+
|
|
164
|
+
### Dependencies
|
|
165
|
+
|
|
166
|
+
> **IMPORTANT:** if you add a new dependency to the application as described
|
|
167
|
+
> below you will need to run `docker compose build` or add `--build` to the
|
|
168
|
+
> `docker compose run` and/or `docker compose up` command at least once for
|
|
169
|
+
> changes to take effect when running code inside containers. The poe tasks have
|
|
170
|
+
> already got `--build` appended to the command line.
|
|
171
|
+
|
|
172
|
+
To add a new dependency _for the application itself_:
|
|
173
|
+
|
|
174
|
+
```console
|
|
175
|
+
poetry add {dependency}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
To add a new development-time dependency _used only when the application is
|
|
179
|
+
running locally in development or in testing_:
|
|
180
|
+
|
|
181
|
+
```console
|
|
182
|
+
poetry add -G dev {dependency}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
To remove a dependency which is no longer needed:
|
|
186
|
+
|
|
187
|
+
```console
|
|
188
|
+
poetry remove {dependency}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## CI configuration
|
|
192
|
+
|
|
193
|
+
The project is configured with Gitlab AutoDevOps via Gitlab CI using the .gitlab-ci.yml file.
|
|
194
|
+
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# UCam FaaS Library
|
|
2
|
+
|
|
3
|
+
This project contains a support library and base Docker image to be used to
|
|
4
|
+
create Function as a Service (FaaS) applications intended to be deployed to a
|
|
5
|
+
GCP cloud run environment.
|
|
6
|
+
|
|
7
|
+
It is highly opinionated and non-configurable by design.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
Install the library via pip:
|
|
12
|
+
|
|
13
|
+
```console
|
|
14
|
+
pip install ucam-faas
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Install the library with testing support:
|
|
18
|
+
|
|
19
|
+
```console
|
|
20
|
+
pip install ucam-faas[testing]
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The library provides a decorator to create a runnable application from a single
|
|
24
|
+
function. The function must accept a dictionary as an argument, and return
|
|
25
|
+
either a string or a dictionary as a response:
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
# main.py
|
|
29
|
+
from ucam_faas import event_handler
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@event_handler
|
|
33
|
+
def say_hello(event):
|
|
34
|
+
return "hello!"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This can then be run as a FaaS app using:
|
|
38
|
+
|
|
39
|
+
```console
|
|
40
|
+
ucam-faas --debug --target say_hello
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Testing FaaS Functions
|
|
44
|
+
|
|
45
|
+
To unit test FaaS functions there are two available approaches. Firstly, the
|
|
46
|
+
unwrapped version of the function can be directly accessed. This is recommended
|
|
47
|
+
as the primary way to test FaaS functions and requires no additional
|
|
48
|
+
configuration:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
# test_my_event_handler.py
|
|
52
|
+
from main import say_hello
|
|
53
|
+
|
|
54
|
+
def test_say_hello():
|
|
55
|
+
assert say_hello.__wrapped__({"event_key": "event_value"}) == "hello!"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The original function version is made available through the `__wrapped__`
|
|
59
|
+
variable.
|
|
60
|
+
|
|
61
|
+
Alternatively, if required, a support testing client can be used to instantiate
|
|
62
|
+
a version of the web application running the function. To do this the extra
|
|
63
|
+
"testing" must also be installed:
|
|
64
|
+
|
|
65
|
+
```shell
|
|
66
|
+
pip install ucam-faas[testing]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Then tests can register the provided `pytest` fixture and use it in tests:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
# test_my_event_handler.py
|
|
73
|
+
pytest_plugins = ["ucam_faas.testing"]
|
|
74
|
+
|
|
75
|
+
def test_say_hello(event_app_client):
|
|
76
|
+
# Provide the target function for the test webapp
|
|
77
|
+
eac = event_app_client("say_hello")
|
|
78
|
+
response = eac.get("/")
|
|
79
|
+
assert response.status_code == 200
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Note that with this approach it is not necessary to import the function under
|
|
83
|
+
test, it is discovered and imported during the test webapp setup.
|
|
84
|
+
|
|
85
|
+
### Example
|
|
86
|
+
|
|
87
|
+
An example application and example tests can be found in this repository in the
|
|
88
|
+
`example` directory.
|
|
89
|
+
|
|
90
|
+
Note that the example dockerfile uses a relative file `FROM` - this means it
|
|
91
|
+
must be built in the context of its parent directory:
|
|
92
|
+
|
|
93
|
+
```console
|
|
94
|
+
docker build -f example/Dockerfile .
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Local Development
|
|
98
|
+
|
|
99
|
+
Install poetry via:
|
|
100
|
+
|
|
101
|
+
```console
|
|
102
|
+
pipx install poetry
|
|
103
|
+
pipx inject poetry poethepoet[poetry_plugin]
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Install dependencies via:
|
|
107
|
+
|
|
108
|
+
```console
|
|
109
|
+
poetry install
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Build the library via:
|
|
113
|
+
|
|
114
|
+
```console
|
|
115
|
+
poetry build
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Run the example application via:
|
|
119
|
+
|
|
120
|
+
```console
|
|
121
|
+
poetry poe example
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Run the tests via:
|
|
125
|
+
|
|
126
|
+
```console
|
|
127
|
+
poetry poe tox
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Note that the tests are found under the example directory, there are *currently*
|
|
131
|
+
no tests in the root library as the code is predominantly configuration and
|
|
132
|
+
setup, and example testing has been deemed sufficient.
|
|
133
|
+
|
|
134
|
+
### Dependencies
|
|
135
|
+
|
|
136
|
+
> **IMPORTANT:** if you add a new dependency to the application as described
|
|
137
|
+
> below you will need to run `docker compose build` or add `--build` to the
|
|
138
|
+
> `docker compose run` and/or `docker compose up` command at least once for
|
|
139
|
+
> changes to take effect when running code inside containers. The poe tasks have
|
|
140
|
+
> already got `--build` appended to the command line.
|
|
141
|
+
|
|
142
|
+
To add a new dependency _for the application itself_:
|
|
143
|
+
|
|
144
|
+
```console
|
|
145
|
+
poetry add {dependency}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
To add a new development-time dependency _used only when the application is
|
|
149
|
+
running locally in development or in testing_:
|
|
150
|
+
|
|
151
|
+
```console
|
|
152
|
+
poetry add -G dev {dependency}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
To remove a dependency which is no longer needed:
|
|
156
|
+
|
|
157
|
+
```console
|
|
158
|
+
poetry remove {dependency}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## CI configuration
|
|
162
|
+
|
|
163
|
+
The project is configured with Gitlab AutoDevOps via Gitlab CI using the .gitlab-ci.yml file.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "ucam-faas"
|
|
3
|
+
version = "0.1.2"
|
|
4
|
+
description = "Opinionated FaaS support framework extending Google's functions-framework"
|
|
5
|
+
authors = ["University of Cambridge Information Services <devops-wilson@uis.cam.ac.uk>"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
repository = "https://gitlab.developers.cam.ac.uk/uis/devops/ucam-faas-python/ucam-faas"
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Programming Language :: Python :: 3",
|
|
11
|
+
"License :: OSI Approved :: MIT License",
|
|
12
|
+
"Development Status :: 3 - Alpha",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Operating System :: OS Independent",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[tool.poetry.scripts]
|
|
19
|
+
ucam-faas = "ucam_faas:_cli"
|
|
20
|
+
|
|
21
|
+
[tool.poe.tasks.example]
|
|
22
|
+
help = "Run the example application"
|
|
23
|
+
cmd = "docker compose up --build"
|
|
24
|
+
env = { COMPOSE_PROFILES = "example" }
|
|
25
|
+
|
|
26
|
+
[tool.poe.tasks.fix]
|
|
27
|
+
help = "Run pre-commit checks to fix formatting errors"
|
|
28
|
+
cmd = "pre-commit run --all-files"
|
|
29
|
+
|
|
30
|
+
[tool.poe.tasks.tox]
|
|
31
|
+
help = "Run the Python test suite via tox"
|
|
32
|
+
cmd = "docker compose run --build --rm tox"
|
|
33
|
+
|
|
34
|
+
[tool.poe.tasks.down]
|
|
35
|
+
help = "Stop any running containers"
|
|
36
|
+
cmd = "docker compose down"
|
|
37
|
+
env = { COMPOSE_PROFILES = "example,tox" }
|
|
38
|
+
|
|
39
|
+
[tool.poe.tasks."compose:build"]
|
|
40
|
+
help = "Build or rebuild all container images"
|
|
41
|
+
cmd = "docker compose build"
|
|
42
|
+
env = { COMPOSE_PROFILES = "example,tox" }
|
|
43
|
+
|
|
44
|
+
[tool.poe.tasks."compose:pull"]
|
|
45
|
+
help = "Pull any upstream container images"
|
|
46
|
+
cmd = "docker compose pull --ignore-buildable --ignore-pull-failures"
|
|
47
|
+
env = { COMPOSE_PROFILES = "example,tox" }
|
|
48
|
+
|
|
49
|
+
[tool.poe.tasks."tox:local"]
|
|
50
|
+
help = "Run the Python test suite via tox using the locally installed Python version"
|
|
51
|
+
cmd = "tox"
|
|
52
|
+
|
|
53
|
+
[tool.poe.tasks."pytest:local"]
|
|
54
|
+
help = "Run the Python test suite via pytest using the locally installed Python version"
|
|
55
|
+
cmd = "pytest example/tests.py"
|
|
56
|
+
|
|
57
|
+
[tool.poetry.dependencies]
|
|
58
|
+
python = "^3.9"
|
|
59
|
+
requests = "^2.31.0"
|
|
60
|
+
structlog = "^24.1.0"
|
|
61
|
+
functions-framework = "^3.5.0"
|
|
62
|
+
click = "^8.1.7"
|
|
63
|
+
gunicorn = "^22.0.0"
|
|
64
|
+
flask = "^3.0.3"
|
|
65
|
+
pytest = {version = "^8.1.1", optional = true}
|
|
66
|
+
cloudevents = "^1.10.1"
|
|
67
|
+
|
|
68
|
+
[tool.poetry.group.dev.dependencies]
|
|
69
|
+
pytest-cov = "^4.1.0"
|
|
70
|
+
pre-commit = "^3.6.2"
|
|
71
|
+
tox = "^4.14.2"
|
|
72
|
+
|
|
73
|
+
[tool.poetry.extras]
|
|
74
|
+
testing = ["pytest"]
|
|
75
|
+
|
|
76
|
+
[build-system]
|
|
77
|
+
requires = ["poetry-core"]
|
|
78
|
+
build-backend = "poetry.core.masonry.api"
|
|
79
|
+
|
|
80
|
+
[tool.mypy]
|
|
81
|
+
ignore_missing_imports = true
|
|
82
|
+
|
|
83
|
+
[tool.black]
|
|
84
|
+
line-length = 99
|
|
85
|
+
|
|
86
|
+
[tool.coverage.run]
|
|
87
|
+
omit= [
|
|
88
|
+
".tox/*",
|
|
89
|
+
"setup.py",
|
|
90
|
+
"manage.py",
|
|
91
|
+
"gunicorn.conf.py",
|
|
92
|
+
"example/*",
|
|
93
|
+
"*/test/*",
|
|
94
|
+
"*/tests/*",
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
[tool.isort]
|
|
98
|
+
profile = "black"
|
|
99
|
+
|
|
100
|
+
[tool.pytest.ini_options]
|
|
101
|
+
testpaths = [
|
|
102
|
+
"example/test.py",
|
|
103
|
+
"**/tests/*.py",
|
|
104
|
+
"**/test/*.py",
|
|
105
|
+
]
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import logging.config
|
|
3
|
+
import os.path
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
import flask
|
|
7
|
+
import functions_framework
|
|
8
|
+
import gunicorn.app.base
|
|
9
|
+
import structlog
|
|
10
|
+
from cloudevents.http.event import CloudEvent
|
|
11
|
+
from gunicorn.config import get_default_config_file
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def raw_event(function):
|
|
15
|
+
def _raw_event_internal(request: flask.Request) -> flask.typing.ResponseReturnValue:
|
|
16
|
+
return function(request.data)
|
|
17
|
+
|
|
18
|
+
_raw_event_internal.__name__ = function.__name__
|
|
19
|
+
_raw_event_internal = functions_framework.http(_raw_event_internal)
|
|
20
|
+
|
|
21
|
+
_raw_event_internal.__wrapped__ = function
|
|
22
|
+
return _raw_event_internal
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def cloud_event(function):
|
|
26
|
+
def _cloud_event_internal(event: CloudEvent) -> None:
|
|
27
|
+
return function(event.data)
|
|
28
|
+
|
|
29
|
+
_cloud_event_internal.__name__ = function.__name__
|
|
30
|
+
_cloud_event_internal = functions_framework.cloud_event(_cloud_event_internal)
|
|
31
|
+
|
|
32
|
+
_cloud_event_internal.__wrapped__ = function
|
|
33
|
+
return _cloud_event_internal
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class FaaSGunicornApplication(gunicorn.app.base.Application):
|
|
37
|
+
_BASE_CONFIG = "/etc/gunicorn.conf.py"
|
|
38
|
+
|
|
39
|
+
def __init__(self, app, host, port):
|
|
40
|
+
self.host = host
|
|
41
|
+
self.port = port
|
|
42
|
+
self.app = app
|
|
43
|
+
|
|
44
|
+
super().__init__()
|
|
45
|
+
|
|
46
|
+
def load_config(self):
|
|
47
|
+
if os.path.isfile(self._BASE_CONFIG):
|
|
48
|
+
self.load_config_from_file(self._BASE_CONFIG)
|
|
49
|
+
|
|
50
|
+
default_config = get_default_config_file()
|
|
51
|
+
if default_config is not None:
|
|
52
|
+
self.load_config_from_file(default_config)
|
|
53
|
+
|
|
54
|
+
self.cfg.set("bind", f"{self.host}:{self.port}")
|
|
55
|
+
|
|
56
|
+
def load(self):
|
|
57
|
+
return self.app
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _add_log_severity(logger, method_name, event_dict): # pragma: no cover
|
|
61
|
+
"""
|
|
62
|
+
Add the log level to the event dict under the "severity" key.
|
|
63
|
+
|
|
64
|
+
This is used as a structlog log processor, and is necessary as severity is used by GCP instead
|
|
65
|
+
of level.
|
|
66
|
+
|
|
67
|
+
Based on the structlog.stdlib.add_log_level processor.
|
|
68
|
+
"""
|
|
69
|
+
if method_name == "warn":
|
|
70
|
+
method_name = "warning"
|
|
71
|
+
event_dict["severity"] = method_name
|
|
72
|
+
return event_dict
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _configure_structlog(): # pragma: no cover
|
|
76
|
+
"""
|
|
77
|
+
Internal function to configure structlog with a standard set of options.
|
|
78
|
+
"""
|
|
79
|
+
structlog.configure(
|
|
80
|
+
processors=[
|
|
81
|
+
structlog.contextvars.merge_contextvars,
|
|
82
|
+
structlog.stdlib.filter_by_level,
|
|
83
|
+
structlog.processors.TimeStamper(fmt="iso"),
|
|
84
|
+
structlog.stdlib.add_logger_name,
|
|
85
|
+
_add_log_severity,
|
|
86
|
+
structlog.stdlib.PositionalArgumentsFormatter(),
|
|
87
|
+
structlog.processors.StackInfoRenderer(),
|
|
88
|
+
structlog.processors.format_exc_info,
|
|
89
|
+
structlog.processors.UnicodeDecoder(),
|
|
90
|
+
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
|
|
91
|
+
],
|
|
92
|
+
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
93
|
+
cache_logger_on_first_use=True,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _configure_logging(debug): # pragma: no cover
|
|
98
|
+
"""
|
|
99
|
+
Internal function to configure python logging with standard options, and integrate with
|
|
100
|
+
structlog.
|
|
101
|
+
"""
|
|
102
|
+
structlog_foreign_pre_chain = [
|
|
103
|
+
structlog.stdlib.add_log_level,
|
|
104
|
+
structlog.stdlib.add_logger_name,
|
|
105
|
+
structlog.processors.TimeStamper(fmt="iso"),
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
logging.config.dictConfig(
|
|
109
|
+
{
|
|
110
|
+
"version": 1,
|
|
111
|
+
"disable_existing_loggers": True,
|
|
112
|
+
"formatters": {
|
|
113
|
+
# This formatter logs as structured JSON suitable for use in Cloud hosting
|
|
114
|
+
# environments.
|
|
115
|
+
"json_formatter": {
|
|
116
|
+
"()": structlog.stdlib.ProcessorFormatter,
|
|
117
|
+
"processor": structlog.processors.JSONRenderer(),
|
|
118
|
+
"foreign_pre_chain": structlog_foreign_pre_chain,
|
|
119
|
+
},
|
|
120
|
+
# This formatter logs as coloured text suitable for use by humans.
|
|
121
|
+
"console_formatter": {
|
|
122
|
+
"()": structlog.stdlib.ProcessorFormatter,
|
|
123
|
+
"processor": structlog.dev.ConsoleRenderer(colors=True),
|
|
124
|
+
"foreign_pre_chain": structlog_foreign_pre_chain,
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
"handlers": {
|
|
128
|
+
"console": {
|
|
129
|
+
"class": "logging.StreamHandler",
|
|
130
|
+
"formatter": "console_formatter",
|
|
131
|
+
},
|
|
132
|
+
"json": {
|
|
133
|
+
"class": "logging.StreamHandler",
|
|
134
|
+
"formatter": "json_formatter",
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
"loggers": {
|
|
138
|
+
"": {
|
|
139
|
+
"handlers": ["console" if debug else "json"],
|
|
140
|
+
"propagate": True,
|
|
141
|
+
"level": "INFO",
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _initialize_logging(debug): # pragma: no cover
|
|
149
|
+
"""
|
|
150
|
+
Internal function to initialise logging, configuring python logging and structlog.
|
|
151
|
+
"""
|
|
152
|
+
_configure_structlog()
|
|
153
|
+
_configure_logging(debug)
|
|
154
|
+
logger = logging.getLogger()
|
|
155
|
+
return logger.handlers[:], structlog.wrap_logger(logger)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _initialize_ucam_faas_app(target, source, debug):
|
|
159
|
+
handlers, logger = _initialize_logging(debug)
|
|
160
|
+
app = functions_framework.create_app(target, source)
|
|
161
|
+
app.logger.handlers = handlers
|
|
162
|
+
|
|
163
|
+
@app.route("/healthy")
|
|
164
|
+
@app.route("/status")
|
|
165
|
+
def get_status():
|
|
166
|
+
return "ok"
|
|
167
|
+
|
|
168
|
+
return app
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def run_ucam_faas(target, source, host, port, debug): # pragma: no cover
|
|
172
|
+
app = _initialize_ucam_faas_app(target, source, debug)
|
|
173
|
+
if debug:
|
|
174
|
+
app.run(host, port, debug)
|
|
175
|
+
else:
|
|
176
|
+
server = FaaSGunicornApplication(app, host, port)
|
|
177
|
+
server.run()
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@click.command()
|
|
181
|
+
@click.option("--target", envvar="FUNCTION_TARGET", type=click.STRING, required=True)
|
|
182
|
+
@click.option("--source", envvar="FUNCTION_SOURCE", type=click.Path(), default=None)
|
|
183
|
+
@click.option("--host", envvar="HOST", type=click.STRING, default="0.0.0.0")
|
|
184
|
+
@click.option("--port", envvar="PORT", type=click.INT, default=8080)
|
|
185
|
+
@click.option("--debug", envvar="DEBUG", is_flag=True)
|
|
186
|
+
def _cli(target, source, host, port, debug): # pragma: no cover
|
|
187
|
+
run_ucam_faas(target, source, host, port, debug)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from . import _initialize_ucam_faas_app
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
from pytest import fixture
|
|
5
|
+
|
|
6
|
+
@fixture
|
|
7
|
+
def event_app_client():
|
|
8
|
+
def _event_app_client(target, source=None):
|
|
9
|
+
test_app = _initialize_ucam_faas_app(target, source, True)
|
|
10
|
+
return test_app.test_client()
|
|
11
|
+
|
|
12
|
+
return _event_app_client
|
|
13
|
+
|
|
14
|
+
except ImportError:
|
|
15
|
+
pass
|
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from flask import Flask
|
|
2
|
+
|
|
3
|
+
from ucam_faas import FaaSGunicornApplication
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_faas_gunicorn_application_setup():
|
|
7
|
+
app = Flask(__name__)
|
|
8
|
+
|
|
9
|
+
application = FaaSGunicornApplication(app, "0.0.0.0", "8080")
|
|
10
|
+
|
|
11
|
+
# Set by __init__:
|
|
12
|
+
assert application.cfg.bind == ["0.0.0.0:8080"]
|
|
13
|
+
# Read from the default 'gunicorn.conf.py' file:
|
|
14
|
+
assert application.cfg.logconfig_dict["version"] == 1
|
|
15
|
+
assert (
|
|
16
|
+
len(application.cfg.logconfig_dict["formatters"]["json_formatter"]["foreign_pre_chain"])
|
|
17
|
+
== 3
|
|
18
|
+
)
|