functions-framework 3.5.0__tar.gz → 3.7.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.
- {functions-framework-3.5.0 → functions_framework-3.7.0}/PKG-INFO +5 -5
- {functions-framework-3.5.0 → functions_framework-3.7.0}/setup.py +5 -5
- {functions-framework-3.5.0 → functions_framework-3.7.0}/src/functions_framework/__init__.py +45 -7
- functions_framework-3.7.0/src/functions_framework/_http/gunicorn.py +72 -0
- {functions-framework-3.5.0 → functions_framework-3.7.0}/src/functions_framework/_typed_event.py +3 -3
- {functions-framework-3.5.0 → functions_framework-3.7.0}/src/functions_framework/exceptions.py +5 -1
- functions_framework-3.7.0/src/functions_framework/execution_id.py +156 -0
- functions_framework-3.7.0/src/functions_framework/request_timeout.py +42 -0
- {functions-framework-3.5.0 → functions_framework-3.7.0}/src/functions_framework.egg-info/PKG-INFO +5 -5
- {functions-framework-3.5.0 → functions_framework-3.7.0}/src/functions_framework.egg-info/SOURCES.txt +4 -0
- {functions-framework-3.5.0 → functions_framework-3.7.0}/src/functions_framework.egg-info/requires.txt +1 -0
- functions_framework-3.7.0/tests/test_execution_id.py +382 -0
- {functions-framework-3.5.0 → functions_framework-3.7.0}/tests/test_samples.py +1 -1
- functions_framework-3.7.0/tests/test_timeouts.py +265 -0
- {functions-framework-3.5.0 → functions_framework-3.7.0}/tests/test_view_functions.py +1 -1
- functions-framework-3.5.0/src/functions_framework/_http/gunicorn.py +0 -39
- {functions-framework-3.5.0 → functions_framework-3.7.0}/LICENSE +0 -0
- {functions-framework-3.5.0 → functions_framework-3.7.0}/README.md +0 -0
- {functions-framework-3.5.0 → functions_framework-3.7.0}/setup.cfg +0 -0
- {functions-framework-3.5.0 → functions_framework-3.7.0}/src/functions_framework/__main__.py +0 -0
- {functions-framework-3.5.0 → functions_framework-3.7.0}/src/functions_framework/_cli.py +0 -0
- {functions-framework-3.5.0 → functions_framework-3.7.0}/src/functions_framework/_function_registry.py +0 -0
- {functions-framework-3.5.0 → functions_framework-3.7.0}/src/functions_framework/_http/__init__.py +0 -0
- {functions-framework-3.5.0 → functions_framework-3.7.0}/src/functions_framework/_http/flask.py +0 -0
- {functions-framework-3.5.0 → functions_framework-3.7.0}/src/functions_framework/background_event.py +0 -0
- {functions-framework-3.5.0 → functions_framework-3.7.0}/src/functions_framework/event_conversion.py +0 -0
- {functions-framework-3.5.0 → functions_framework-3.7.0}/src/functions_framework/py.typed +0 -0
- {functions-framework-3.5.0 → functions_framework-3.7.0}/src/functions_framework.egg-info/dependency_links.txt +0 -0
- {functions-framework-3.5.0 → functions_framework-3.7.0}/src/functions_framework.egg-info/entry_points.txt +0 -0
- {functions-framework-3.5.0 → functions_framework-3.7.0}/src/functions_framework.egg-info/namespace_packages.txt +0 -0
- {functions-framework-3.5.0 → functions_framework-3.7.0}/src/functions_framework.egg-info/top_level.txt +0 -0
- {functions-framework-3.5.0 → functions_framework-3.7.0}/src/google/__init__.py +0 -0
- {functions-framework-3.5.0 → functions_framework-3.7.0}/src/google/cloud/__init__.py +0 -0
- {functions-framework-3.5.0 → functions_framework-3.7.0}/src/google/cloud/functions/__init__.py +0 -0
- {functions-framework-3.5.0 → functions_framework-3.7.0}/src/google/cloud/functions/context.py +0 -0
- {functions-framework-3.5.0 → functions_framework-3.7.0}/src/google/cloud/functions_v1/__init__.py +0 -0
- {functions-framework-3.5.0 → functions_framework-3.7.0}/src/google/cloud/functions_v1/context.py +0 -0
- {functions-framework-3.5.0 → functions_framework-3.7.0}/src/google/cloud/functions_v1beta2/__init__.py +0 -0
- {functions-framework-3.5.0 → functions_framework-3.7.0}/src/google/cloud/functions_v1beta2/context.py +0 -0
- {functions-framework-3.5.0 → functions_framework-3.7.0}/tests/test_cli.py +0 -0
- {functions-framework-3.5.0 → functions_framework-3.7.0}/tests/test_cloud_event_functions.py +0 -0
- {functions-framework-3.5.0 → functions_framework-3.7.0}/tests/test_convert.py +0 -0
- {functions-framework-3.5.0 → functions_framework-3.7.0}/tests/test_decorator_functions.py +0 -0
- {functions-framework-3.5.0 → functions_framework-3.7.0}/tests/test_function_registry.py +0 -0
- {functions-framework-3.5.0 → functions_framework-3.7.0}/tests/test_functions.py +0 -0
- {functions-framework-3.5.0 → functions_framework-3.7.0}/tests/test_http.py +0 -0
- {functions-framework-3.5.0 → functions_framework-3.7.0}/tests/test_main.py +0 -0
- {functions-framework-3.5.0 → functions_framework-3.7.0}/tests/test_typed_event_functions.py +0 -0
- {functions-framework-3.5.0 → functions_framework-3.7.0}/tests/test_typing.py +0 -0
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: functions-framework
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.7.0
|
|
4
4
|
Summary: An open source FaaS (Function as a service) framework for writing portable Python functions -- brought to you by the Google Cloud Functions team.
|
|
5
5
|
Home-page: https://github.com/googlecloudplatform/functions-framework-python
|
|
6
6
|
Author: Google LLC
|
|
7
7
|
Author-email: googleapis-packages@google.com
|
|
8
8
|
Keywords: functions-framework
|
|
9
|
-
Classifier: Development Status ::
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
10
|
Classifier: Intended Audience :: Developers
|
|
11
11
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
12
|
-
Classifier: Programming Language :: Python :: 3
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.5
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.6
|
|
15
12
|
Classifier: Programming Language :: Python :: 3.7
|
|
16
13
|
Classifier: Programming Language :: Python :: 3.8
|
|
17
14
|
Classifier: Programming Language :: Python :: 3.9
|
|
18
15
|
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
18
|
Requires-Python: >=3.5, <4
|
|
20
19
|
Description-Content-Type: text/markdown
|
|
21
20
|
License-File: LICENSE
|
|
@@ -24,6 +23,7 @@ Requires-Dist: click<9.0,>=7.0
|
|
|
24
23
|
Requires-Dist: watchdog>=1.0.0
|
|
25
24
|
Requires-Dist: gunicorn>=19.2.0; platform_system != "Windows"
|
|
26
25
|
Requires-Dist: cloudevents<2.0.0,>=1.2.0
|
|
26
|
+
Requires-Dist: Werkzeug<4.0.0,>=0.14
|
|
27
27
|
|
|
28
28
|
# Functions Framework for Python
|
|
29
29
|
|
|
@@ -25,7 +25,7 @@ with open(path.join(here, "README.md"), encoding="utf-8") as f:
|
|
|
25
25
|
|
|
26
26
|
setup(
|
|
27
27
|
name="functions-framework",
|
|
28
|
-
version="3.
|
|
28
|
+
version="3.7.0",
|
|
29
29
|
description="An open source FaaS (Function as a service) framework for writing portable Python functions -- brought to you by the Google Cloud Functions team.",
|
|
30
30
|
long_description=long_description,
|
|
31
31
|
long_description_content_type="text/markdown",
|
|
@@ -33,16 +33,15 @@ setup(
|
|
|
33
33
|
author="Google LLC",
|
|
34
34
|
author_email="googleapis-packages@google.com",
|
|
35
35
|
classifiers=[
|
|
36
|
-
"Development Status ::
|
|
36
|
+
"Development Status :: 5 - Production/Stable ",
|
|
37
37
|
"Intended Audience :: Developers",
|
|
38
38
|
"License :: OSI Approved :: Apache Software License",
|
|
39
|
-
"Programming Language :: Python :: 3",
|
|
40
|
-
"Programming Language :: Python :: 3.5",
|
|
41
|
-
"Programming Language :: Python :: 3.6",
|
|
42
39
|
"Programming Language :: Python :: 3.7",
|
|
43
40
|
"Programming Language :: Python :: 3.8",
|
|
44
41
|
"Programming Language :: Python :: 3.9",
|
|
45
42
|
"Programming Language :: Python :: 3.10",
|
|
43
|
+
"Programming Language :: Python :: 3.11",
|
|
44
|
+
"Programming Language :: Python :: 3.12",
|
|
46
45
|
],
|
|
47
46
|
keywords="functions-framework",
|
|
48
47
|
packages=find_packages(where="src"),
|
|
@@ -56,6 +55,7 @@ setup(
|
|
|
56
55
|
"watchdog>=1.0.0",
|
|
57
56
|
"gunicorn>=19.2.0; platform_system!='Windows'",
|
|
58
57
|
"cloudevents>=1.2.0,<2.0.0",
|
|
58
|
+
"Werkzeug>=0.14,<4.0.0",
|
|
59
59
|
],
|
|
60
60
|
entry_points={
|
|
61
61
|
"console_scripts": [
|
|
@@ -17,6 +17,8 @@ import inspect
|
|
|
17
17
|
import io
|
|
18
18
|
import json
|
|
19
19
|
import logging
|
|
20
|
+
import logging.config
|
|
21
|
+
import os
|
|
20
22
|
import os.path
|
|
21
23
|
import pathlib
|
|
22
24
|
import sys
|
|
@@ -32,7 +34,12 @@ import werkzeug
|
|
|
32
34
|
from cloudevents.http import from_http, is_binary
|
|
33
35
|
from cloudevents.http.event import CloudEvent
|
|
34
36
|
|
|
35
|
-
from functions_framework import
|
|
37
|
+
from functions_framework import (
|
|
38
|
+
_function_registry,
|
|
39
|
+
_typed_event,
|
|
40
|
+
event_conversion,
|
|
41
|
+
execution_id,
|
|
42
|
+
)
|
|
36
43
|
from functions_framework.background_event import BackgroundEvent
|
|
37
44
|
from functions_framework.exceptions import (
|
|
38
45
|
EventConversionException,
|
|
@@ -65,9 +72,9 @@ class _LoggingHandler(io.TextIOWrapper):
|
|
|
65
72
|
|
|
66
73
|
def cloud_event(func: CloudEventFunction) -> CloudEventFunction:
|
|
67
74
|
"""Decorator that registers cloudevent as user function signature type."""
|
|
68
|
-
_function_registry.REGISTRY_MAP[
|
|
69
|
-
|
|
70
|
-
|
|
75
|
+
_function_registry.REGISTRY_MAP[func.__name__] = (
|
|
76
|
+
_function_registry.CLOUDEVENT_SIGNATURE_TYPE
|
|
77
|
+
)
|
|
71
78
|
|
|
72
79
|
@functools.wraps(func)
|
|
73
80
|
def wrapper(*args, **kwargs):
|
|
@@ -105,9 +112,9 @@ def typed(*args):
|
|
|
105
112
|
|
|
106
113
|
def http(func: HTTPFunction) -> HTTPFunction:
|
|
107
114
|
"""Decorator that registers http as user function signature type."""
|
|
108
|
-
_function_registry.REGISTRY_MAP[
|
|
109
|
-
|
|
110
|
-
|
|
115
|
+
_function_registry.REGISTRY_MAP[func.__name__] = (
|
|
116
|
+
_function_registry.HTTP_SIGNATURE_TYPE
|
|
117
|
+
)
|
|
111
118
|
|
|
112
119
|
@functools.wraps(func)
|
|
113
120
|
def wrapper(*args, **kwargs):
|
|
@@ -129,6 +136,7 @@ def setup_logging():
|
|
|
129
136
|
|
|
130
137
|
|
|
131
138
|
def _http_view_func_wrapper(function, request):
|
|
139
|
+
@execution_id.set_execution_context(request, _enable_execution_id_logging())
|
|
132
140
|
@functools.wraps(function)
|
|
133
141
|
def view_func(path):
|
|
134
142
|
return function(request._get_current_object())
|
|
@@ -143,6 +151,7 @@ def _run_cloud_event(function, request):
|
|
|
143
151
|
|
|
144
152
|
|
|
145
153
|
def _typed_event_func_wrapper(function, request, inputType: Type):
|
|
154
|
+
@execution_id.set_execution_context(request, _enable_execution_id_logging())
|
|
146
155
|
def view_func(path):
|
|
147
156
|
try:
|
|
148
157
|
data = request.get_json()
|
|
@@ -163,6 +172,7 @@ def _typed_event_func_wrapper(function, request, inputType: Type):
|
|
|
163
172
|
|
|
164
173
|
|
|
165
174
|
def _cloud_event_view_func_wrapper(function, request):
|
|
175
|
+
@execution_id.set_execution_context(request, _enable_execution_id_logging())
|
|
166
176
|
def view_func(path):
|
|
167
177
|
ce_exception = None
|
|
168
178
|
event = None
|
|
@@ -198,6 +208,7 @@ def _cloud_event_view_func_wrapper(function, request):
|
|
|
198
208
|
|
|
199
209
|
|
|
200
210
|
def _event_view_func_wrapper(function, request):
|
|
211
|
+
@execution_id.set_execution_context(request, _enable_execution_id_logging())
|
|
201
212
|
def view_func(path):
|
|
202
213
|
if event_conversion.is_convertable_cloud_event(request):
|
|
203
214
|
# Convert this CloudEvent to the equivalent background event data and context.
|
|
@@ -332,6 +343,9 @@ def create_app(target=None, source=None, signature_type=None):
|
|
|
332
343
|
|
|
333
344
|
source_module, spec = _function_registry.load_function_module(source)
|
|
334
345
|
|
|
346
|
+
if _enable_execution_id_logging():
|
|
347
|
+
_configure_app_execution_id_logging()
|
|
348
|
+
|
|
335
349
|
# Create the application
|
|
336
350
|
_app = flask.Flask(target, template_folder=template_folder)
|
|
337
351
|
_app.register_error_handler(500, crash_handler)
|
|
@@ -355,6 +369,7 @@ def create_app(target=None, source=None, signature_type=None):
|
|
|
355
369
|
sys.stderr = _LoggingHandler("ERROR", sys.stderr)
|
|
356
370
|
setup_logging()
|
|
357
371
|
|
|
372
|
+
_app.wsgi_app = execution_id.WsgiMiddleware(_app.wsgi_app)
|
|
358
373
|
# Execute the module, within the application context
|
|
359
374
|
with _app.app_context():
|
|
360
375
|
try:
|
|
@@ -411,6 +426,29 @@ class LazyWSGIApp:
|
|
|
411
426
|
return self.app(*args, **kwargs)
|
|
412
427
|
|
|
413
428
|
|
|
429
|
+
def _configure_app_execution_id_logging():
|
|
430
|
+
# Logging needs to be configured before app logger is accessed
|
|
431
|
+
logging.config.dictConfig(
|
|
432
|
+
{
|
|
433
|
+
"version": 1,
|
|
434
|
+
"handlers": {
|
|
435
|
+
"wsgi": {
|
|
436
|
+
"class": "logging.StreamHandler",
|
|
437
|
+
"stream": "ext://functions_framework.execution_id.logging_stream",
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
"root": {"level": "INFO", "handlers": ["wsgi"]},
|
|
441
|
+
}
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _enable_execution_id_logging():
|
|
446
|
+
# Based on distutils.util.strtobool
|
|
447
|
+
truthy_values = ("y", "yes", "t", "true", "on", "1")
|
|
448
|
+
env_var_value = os.environ.get("LOG_EXECUTION_ID")
|
|
449
|
+
return env_var_value in truthy_values
|
|
450
|
+
|
|
451
|
+
|
|
414
452
|
app = LazyWSGIApp()
|
|
415
453
|
|
|
416
454
|
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Copyright 2024 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
|
|
17
|
+
import gunicorn.app.base
|
|
18
|
+
|
|
19
|
+
from gunicorn.workers.gthread import ThreadWorker
|
|
20
|
+
|
|
21
|
+
from ..request_timeout import ThreadingTimeout
|
|
22
|
+
|
|
23
|
+
# global for use in our custom gthread worker; the gunicorn arbiter spawns these
|
|
24
|
+
# and it's not possible to inject (and self.timeout means something different to
|
|
25
|
+
# async workers!)
|
|
26
|
+
# set/managed in gunicorn application init for test-friendliness
|
|
27
|
+
TIMEOUT_SECONDS = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class GunicornApplication(gunicorn.app.base.BaseApplication):
|
|
31
|
+
def __init__(self, app, host, port, debug, **options):
|
|
32
|
+
threads = int(os.environ.get("THREADS", (os.cpu_count() or 1) * 4))
|
|
33
|
+
|
|
34
|
+
global TIMEOUT_SECONDS
|
|
35
|
+
TIMEOUT_SECONDS = int(os.environ.get("CLOUD_RUN_TIMEOUT_SECONDS", 0))
|
|
36
|
+
|
|
37
|
+
self.options = {
|
|
38
|
+
"bind": "%s:%s" % (host, port),
|
|
39
|
+
"workers": int(os.environ.get("WORKERS", 1)),
|
|
40
|
+
"threads": threads,
|
|
41
|
+
"loglevel": os.environ.get("GUNICORN_LOG_LEVEL", "error"),
|
|
42
|
+
"limit_request_line": 0,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (
|
|
46
|
+
TIMEOUT_SECONDS > 0
|
|
47
|
+
and threads > 1
|
|
48
|
+
and (os.environ.get("THREADED_TIMEOUT_ENABLED", "False").lower() == "true")
|
|
49
|
+
): # pragma: no cover
|
|
50
|
+
self.options["worker_class"] = (
|
|
51
|
+
"functions_framework._http.gunicorn.GThreadWorkerWithTimeoutSupport"
|
|
52
|
+
)
|
|
53
|
+
else:
|
|
54
|
+
self.options["timeout"] = TIMEOUT_SECONDS
|
|
55
|
+
|
|
56
|
+
self.options.update(options)
|
|
57
|
+
self.app = app
|
|
58
|
+
|
|
59
|
+
super().__init__()
|
|
60
|
+
|
|
61
|
+
def load_config(self):
|
|
62
|
+
for key, value in self.options.items():
|
|
63
|
+
self.cfg.set(key, value)
|
|
64
|
+
|
|
65
|
+
def load(self):
|
|
66
|
+
return self.app
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class GThreadWorkerWithTimeoutSupport(ThreadWorker): # pragma: no cover
|
|
70
|
+
def handle_request(self, req, conn):
|
|
71
|
+
with ThreadingTimeout(TIMEOUT_SECONDS):
|
|
72
|
+
super(GThreadWorkerWithTimeoutSupport, self).handle_request(req, conn)
|
{functions-framework-3.5.0 → functions_framework-3.7.0}/src/functions_framework/_typed_event.py
RENAMED
|
@@ -48,9 +48,9 @@ def register_typed_event(decorator_type, func):
|
|
|
48
48
|
)
|
|
49
49
|
|
|
50
50
|
_function_registry.INPUT_TYPE_MAP[func.__name__] = input_type
|
|
51
|
-
_function_registry.REGISTRY_MAP[
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
_function_registry.REGISTRY_MAP[func.__name__] = (
|
|
52
|
+
_function_registry.TYPED_SIGNATURE_TYPE
|
|
53
|
+
)
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
""" Checks whether the response type of the typed function has a to_dict method"""
|
{functions-framework-3.5.0 → functions_framework-3.7.0}/src/functions_framework/exceptions.py
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright
|
|
1
|
+
# Copyright 2024 Google LLC
|
|
2
2
|
#
|
|
3
3
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
4
|
# you may not use this file except in compliance with the License.
|
|
@@ -35,3 +35,7 @@ class MissingTargetException(FunctionsFrameworkException):
|
|
|
35
35
|
|
|
36
36
|
class EventConversionException(FunctionsFrameworkException):
|
|
37
37
|
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class RequestTimeoutException(FunctionsFrameworkException):
|
|
41
|
+
pass
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Copyright 2020 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import contextlib
|
|
16
|
+
import functools
|
|
17
|
+
import io
|
|
18
|
+
import json
|
|
19
|
+
import logging
|
|
20
|
+
import random
|
|
21
|
+
import re
|
|
22
|
+
import string
|
|
23
|
+
import sys
|
|
24
|
+
|
|
25
|
+
import flask
|
|
26
|
+
|
|
27
|
+
from werkzeug.local import LocalProxy
|
|
28
|
+
|
|
29
|
+
_EXECUTION_ID_LENGTH = 12
|
|
30
|
+
_EXECUTION_ID_CHARSET = string.digits + string.ascii_letters
|
|
31
|
+
_LOGGING_API_LABELS_FIELD = "logging.googleapis.com/labels"
|
|
32
|
+
_LOGGING_API_SPAN_ID_FIELD = "logging.googleapis.com/spanId"
|
|
33
|
+
_TRACE_CONTEXT_REGEX_PATTERN = re.compile(
|
|
34
|
+
r"^(?P<trace_id>[\w\d]+)/(?P<span_id>\d+);o=(?P<options>[01])$"
|
|
35
|
+
)
|
|
36
|
+
EXECUTION_ID_REQUEST_HEADER = "Function-Execution-Id"
|
|
37
|
+
TRACE_CONTEXT_REQUEST_HEADER = "X-Cloud-Trace-Context"
|
|
38
|
+
|
|
39
|
+
logger = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ExecutionContext:
|
|
43
|
+
def __init__(self, execution_id=None, span_id=None):
|
|
44
|
+
self.execution_id = execution_id
|
|
45
|
+
self.span_id = span_id
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _get_current_context():
|
|
49
|
+
return (
|
|
50
|
+
flask.g.execution_id_context
|
|
51
|
+
if flask.has_request_context() and "execution_id_context" in flask.g
|
|
52
|
+
else None
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _set_current_context(context):
|
|
57
|
+
if flask.has_request_context():
|
|
58
|
+
flask.g.execution_id_context = context
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _generate_execution_id():
|
|
62
|
+
return "".join(
|
|
63
|
+
_EXECUTION_ID_CHARSET[random.randrange(len(_EXECUTION_ID_CHARSET))]
|
|
64
|
+
for _ in range(_EXECUTION_ID_LENGTH)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# Middleware to add execution id to request header if one does not already exist
|
|
69
|
+
class WsgiMiddleware:
|
|
70
|
+
def __init__(self, wsgi_app):
|
|
71
|
+
self.wsgi_app = wsgi_app
|
|
72
|
+
|
|
73
|
+
def __call__(self, environ, start_response):
|
|
74
|
+
execution_id = (
|
|
75
|
+
environ.get("HTTP_FUNCTION_EXECUTION_ID") or _generate_execution_id()
|
|
76
|
+
)
|
|
77
|
+
environ["HTTP_FUNCTION_EXECUTION_ID"] = execution_id
|
|
78
|
+
return self.wsgi_app(environ, start_response)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# Sets execution id and span id for the request
|
|
82
|
+
def set_execution_context(request, enable_id_logging=False):
|
|
83
|
+
if enable_id_logging:
|
|
84
|
+
stdout_redirect = contextlib.redirect_stdout(
|
|
85
|
+
LoggingHandlerAddExecutionId(sys.stdout)
|
|
86
|
+
)
|
|
87
|
+
stderr_redirect = contextlib.redirect_stderr(
|
|
88
|
+
LoggingHandlerAddExecutionId(sys.stderr)
|
|
89
|
+
)
|
|
90
|
+
else:
|
|
91
|
+
stdout_redirect = contextlib.nullcontext()
|
|
92
|
+
stderr_redirect = contextlib.nullcontext()
|
|
93
|
+
|
|
94
|
+
def decorator(view_function):
|
|
95
|
+
@functools.wraps(view_function)
|
|
96
|
+
def wrapper(*args, **kwargs):
|
|
97
|
+
trace_context = re.match(
|
|
98
|
+
_TRACE_CONTEXT_REGEX_PATTERN,
|
|
99
|
+
request.headers.get(TRACE_CONTEXT_REQUEST_HEADER, ""),
|
|
100
|
+
)
|
|
101
|
+
execution_id = request.headers.get(EXECUTION_ID_REQUEST_HEADER)
|
|
102
|
+
span_id = trace_context.group("span_id") if trace_context else None
|
|
103
|
+
_set_current_context(ExecutionContext(execution_id, span_id))
|
|
104
|
+
|
|
105
|
+
with stderr_redirect, stdout_redirect:
|
|
106
|
+
return view_function(*args, **kwargs)
|
|
107
|
+
|
|
108
|
+
return wrapper
|
|
109
|
+
|
|
110
|
+
return decorator
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@LocalProxy
|
|
114
|
+
def logging_stream():
|
|
115
|
+
return LoggingHandlerAddExecutionId(stream=flask.logging.wsgi_errors_stream)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class LoggingHandlerAddExecutionId(io.TextIOWrapper):
|
|
119
|
+
def __new__(cls, stream=sys.stdout):
|
|
120
|
+
if isinstance(stream, LoggingHandlerAddExecutionId):
|
|
121
|
+
return stream
|
|
122
|
+
else:
|
|
123
|
+
return super(LoggingHandlerAddExecutionId, cls).__new__(cls)
|
|
124
|
+
|
|
125
|
+
def __init__(self, stream=sys.stdout):
|
|
126
|
+
io.TextIOWrapper.__init__(self, io.StringIO())
|
|
127
|
+
self.stream = stream
|
|
128
|
+
|
|
129
|
+
def write(self, contents):
|
|
130
|
+
if contents == "\n":
|
|
131
|
+
return
|
|
132
|
+
current_context = _get_current_context()
|
|
133
|
+
if current_context is None:
|
|
134
|
+
self.stream.write(contents + "\n")
|
|
135
|
+
self.stream.flush()
|
|
136
|
+
return
|
|
137
|
+
try:
|
|
138
|
+
execution_id = current_context.execution_id
|
|
139
|
+
span_id = current_context.span_id
|
|
140
|
+
payload = json.loads(contents)
|
|
141
|
+
if not isinstance(payload, dict):
|
|
142
|
+
payload = {"message": contents}
|
|
143
|
+
except json.JSONDecodeError:
|
|
144
|
+
if len(contents) > 0 and contents[-1] == "\n":
|
|
145
|
+
contents = contents[:-1]
|
|
146
|
+
payload = {"message": contents}
|
|
147
|
+
if execution_id:
|
|
148
|
+
payload[_LOGGING_API_LABELS_FIELD] = payload.get(
|
|
149
|
+
_LOGGING_API_LABELS_FIELD, {}
|
|
150
|
+
)
|
|
151
|
+
payload[_LOGGING_API_LABELS_FIELD]["execution_id"] = execution_id
|
|
152
|
+
if span_id:
|
|
153
|
+
payload[_LOGGING_API_SPAN_ID_FIELD] = span_id
|
|
154
|
+
self.stream.write(json.dumps(payload))
|
|
155
|
+
self.stream.write("\n")
|
|
156
|
+
self.stream.flush()
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import ctypes
|
|
2
|
+
import logging
|
|
3
|
+
import threading
|
|
4
|
+
|
|
5
|
+
from .exceptions import RequestTimeoutException
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ThreadingTimeout(object): # pragma: no cover
|
|
11
|
+
def __init__(self, seconds):
|
|
12
|
+
self.seconds = seconds
|
|
13
|
+
self.target_tid = threading.current_thread().ident
|
|
14
|
+
self.timer = None
|
|
15
|
+
|
|
16
|
+
def __enter__(self):
|
|
17
|
+
self.timer = threading.Timer(self.seconds, self._raise_exc)
|
|
18
|
+
self.timer.start()
|
|
19
|
+
return self
|
|
20
|
+
|
|
21
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
22
|
+
self.timer.cancel()
|
|
23
|
+
if exc_type is RequestTimeoutException:
|
|
24
|
+
logger.warning(
|
|
25
|
+
"Request handling exceeded {0} seconds timeout; terminating request handling...".format(
|
|
26
|
+
self.seconds
|
|
27
|
+
),
|
|
28
|
+
exc_info=(exc_type, exc_val, exc_tb),
|
|
29
|
+
)
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
def _raise_exc(self):
|
|
33
|
+
ret = ctypes.pythonapi.PyThreadState_SetAsyncExc(
|
|
34
|
+
ctypes.c_long(self.target_tid), ctypes.py_object(RequestTimeoutException)
|
|
35
|
+
)
|
|
36
|
+
if ret == 0:
|
|
37
|
+
raise ValueError("Invalid thread ID {}".format(self.target_tid))
|
|
38
|
+
elif ret > 1:
|
|
39
|
+
ctypes.pythonapi.PyThreadState_SetAsyncExc(
|
|
40
|
+
ctypes.c_long(self.target_tid), None
|
|
41
|
+
)
|
|
42
|
+
raise SystemError("PyThreadState_SetAsyncExc failed")
|
{functions-framework-3.5.0 → functions_framework-3.7.0}/src/functions_framework.egg-info/PKG-INFO
RENAMED
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: functions-framework
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.7.0
|
|
4
4
|
Summary: An open source FaaS (Function as a service) framework for writing portable Python functions -- brought to you by the Google Cloud Functions team.
|
|
5
5
|
Home-page: https://github.com/googlecloudplatform/functions-framework-python
|
|
6
6
|
Author: Google LLC
|
|
7
7
|
Author-email: googleapis-packages@google.com
|
|
8
8
|
Keywords: functions-framework
|
|
9
|
-
Classifier: Development Status ::
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
10
|
Classifier: Intended Audience :: Developers
|
|
11
11
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
12
|
-
Classifier: Programming Language :: Python :: 3
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.5
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.6
|
|
15
12
|
Classifier: Programming Language :: Python :: 3.7
|
|
16
13
|
Classifier: Programming Language :: Python :: 3.8
|
|
17
14
|
Classifier: Programming Language :: Python :: 3.9
|
|
18
15
|
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
18
|
Requires-Python: >=3.5, <4
|
|
20
19
|
Description-Content-Type: text/markdown
|
|
21
20
|
License-File: LICENSE
|
|
@@ -24,6 +23,7 @@ Requires-Dist: click<9.0,>=7.0
|
|
|
24
23
|
Requires-Dist: watchdog>=1.0.0
|
|
25
24
|
Requires-Dist: gunicorn>=19.2.0; platform_system != "Windows"
|
|
26
25
|
Requires-Dist: cloudevents<2.0.0,>=1.2.0
|
|
26
|
+
Requires-Dist: Werkzeug<4.0.0,>=0.14
|
|
27
27
|
|
|
28
28
|
# Functions Framework for Python
|
|
29
29
|
|
{functions-framework-3.5.0 → functions_framework-3.7.0}/src/functions_framework.egg-info/SOURCES.txt
RENAMED
|
@@ -10,7 +10,9 @@ src/functions_framework/_typed_event.py
|
|
|
10
10
|
src/functions_framework/background_event.py
|
|
11
11
|
src/functions_framework/event_conversion.py
|
|
12
12
|
src/functions_framework/exceptions.py
|
|
13
|
+
src/functions_framework/execution_id.py
|
|
13
14
|
src/functions_framework/py.typed
|
|
15
|
+
src/functions_framework/request_timeout.py
|
|
14
16
|
src/functions_framework.egg-info/PKG-INFO
|
|
15
17
|
src/functions_framework.egg-info/SOURCES.txt
|
|
16
18
|
src/functions_framework.egg-info/dependency_links.txt
|
|
@@ -33,11 +35,13 @@ tests/test_cli.py
|
|
|
33
35
|
tests/test_cloud_event_functions.py
|
|
34
36
|
tests/test_convert.py
|
|
35
37
|
tests/test_decorator_functions.py
|
|
38
|
+
tests/test_execution_id.py
|
|
36
39
|
tests/test_function_registry.py
|
|
37
40
|
tests/test_functions.py
|
|
38
41
|
tests/test_http.py
|
|
39
42
|
tests/test_main.py
|
|
40
43
|
tests/test_samples.py
|
|
44
|
+
tests/test_timeouts.py
|
|
41
45
|
tests/test_typed_event_functions.py
|
|
42
46
|
tests/test_typing.py
|
|
43
47
|
tests/test_view_functions.py
|