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.
Files changed (49) hide show
  1. {functions-framework-3.5.0 → functions_framework-3.7.0}/PKG-INFO +5 -5
  2. {functions-framework-3.5.0 → functions_framework-3.7.0}/setup.py +5 -5
  3. {functions-framework-3.5.0 → functions_framework-3.7.0}/src/functions_framework/__init__.py +45 -7
  4. functions_framework-3.7.0/src/functions_framework/_http/gunicorn.py +72 -0
  5. {functions-framework-3.5.0 → functions_framework-3.7.0}/src/functions_framework/_typed_event.py +3 -3
  6. {functions-framework-3.5.0 → functions_framework-3.7.0}/src/functions_framework/exceptions.py +5 -1
  7. functions_framework-3.7.0/src/functions_framework/execution_id.py +156 -0
  8. functions_framework-3.7.0/src/functions_framework/request_timeout.py +42 -0
  9. {functions-framework-3.5.0 → functions_framework-3.7.0}/src/functions_framework.egg-info/PKG-INFO +5 -5
  10. {functions-framework-3.5.0 → functions_framework-3.7.0}/src/functions_framework.egg-info/SOURCES.txt +4 -0
  11. {functions-framework-3.5.0 → functions_framework-3.7.0}/src/functions_framework.egg-info/requires.txt +1 -0
  12. functions_framework-3.7.0/tests/test_execution_id.py +382 -0
  13. {functions-framework-3.5.0 → functions_framework-3.7.0}/tests/test_samples.py +1 -1
  14. functions_framework-3.7.0/tests/test_timeouts.py +265 -0
  15. {functions-framework-3.5.0 → functions_framework-3.7.0}/tests/test_view_functions.py +1 -1
  16. functions-framework-3.5.0/src/functions_framework/_http/gunicorn.py +0 -39
  17. {functions-framework-3.5.0 → functions_framework-3.7.0}/LICENSE +0 -0
  18. {functions-framework-3.5.0 → functions_framework-3.7.0}/README.md +0 -0
  19. {functions-framework-3.5.0 → functions_framework-3.7.0}/setup.cfg +0 -0
  20. {functions-framework-3.5.0 → functions_framework-3.7.0}/src/functions_framework/__main__.py +0 -0
  21. {functions-framework-3.5.0 → functions_framework-3.7.0}/src/functions_framework/_cli.py +0 -0
  22. {functions-framework-3.5.0 → functions_framework-3.7.0}/src/functions_framework/_function_registry.py +0 -0
  23. {functions-framework-3.5.0 → functions_framework-3.7.0}/src/functions_framework/_http/__init__.py +0 -0
  24. {functions-framework-3.5.0 → functions_framework-3.7.0}/src/functions_framework/_http/flask.py +0 -0
  25. {functions-framework-3.5.0 → functions_framework-3.7.0}/src/functions_framework/background_event.py +0 -0
  26. {functions-framework-3.5.0 → functions_framework-3.7.0}/src/functions_framework/event_conversion.py +0 -0
  27. {functions-framework-3.5.0 → functions_framework-3.7.0}/src/functions_framework/py.typed +0 -0
  28. {functions-framework-3.5.0 → functions_framework-3.7.0}/src/functions_framework.egg-info/dependency_links.txt +0 -0
  29. {functions-framework-3.5.0 → functions_framework-3.7.0}/src/functions_framework.egg-info/entry_points.txt +0 -0
  30. {functions-framework-3.5.0 → functions_framework-3.7.0}/src/functions_framework.egg-info/namespace_packages.txt +0 -0
  31. {functions-framework-3.5.0 → functions_framework-3.7.0}/src/functions_framework.egg-info/top_level.txt +0 -0
  32. {functions-framework-3.5.0 → functions_framework-3.7.0}/src/google/__init__.py +0 -0
  33. {functions-framework-3.5.0 → functions_framework-3.7.0}/src/google/cloud/__init__.py +0 -0
  34. {functions-framework-3.5.0 → functions_framework-3.7.0}/src/google/cloud/functions/__init__.py +0 -0
  35. {functions-framework-3.5.0 → functions_framework-3.7.0}/src/google/cloud/functions/context.py +0 -0
  36. {functions-framework-3.5.0 → functions_framework-3.7.0}/src/google/cloud/functions_v1/__init__.py +0 -0
  37. {functions-framework-3.5.0 → functions_framework-3.7.0}/src/google/cloud/functions_v1/context.py +0 -0
  38. {functions-framework-3.5.0 → functions_framework-3.7.0}/src/google/cloud/functions_v1beta2/__init__.py +0 -0
  39. {functions-framework-3.5.0 → functions_framework-3.7.0}/src/google/cloud/functions_v1beta2/context.py +0 -0
  40. {functions-framework-3.5.0 → functions_framework-3.7.0}/tests/test_cli.py +0 -0
  41. {functions-framework-3.5.0 → functions_framework-3.7.0}/tests/test_cloud_event_functions.py +0 -0
  42. {functions-framework-3.5.0 → functions_framework-3.7.0}/tests/test_convert.py +0 -0
  43. {functions-framework-3.5.0 → functions_framework-3.7.0}/tests/test_decorator_functions.py +0 -0
  44. {functions-framework-3.5.0 → functions_framework-3.7.0}/tests/test_function_registry.py +0 -0
  45. {functions-framework-3.5.0 → functions_framework-3.7.0}/tests/test_functions.py +0 -0
  46. {functions-framework-3.5.0 → functions_framework-3.7.0}/tests/test_http.py +0 -0
  47. {functions-framework-3.5.0 → functions_framework-3.7.0}/tests/test_main.py +0 -0
  48. {functions-framework-3.5.0 → functions_framework-3.7.0}/tests/test_typed_event_functions.py +0 -0
  49. {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.5.0
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 :: 3 - Alpha
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.5.0",
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 :: 3 - Alpha",
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 _function_registry, _typed_event, event_conversion
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
- func.__name__
70
- ] = _function_registry.CLOUDEVENT_SIGNATURE_TYPE
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
- func.__name__
110
- ] = _function_registry.HTTP_SIGNATURE_TYPE
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)
@@ -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
- func.__name__
53
- ] = _function_registry.TYPED_SIGNATURE_TYPE
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"""
@@ -1,4 +1,4 @@
1
- # Copyright 2020 Google LLC
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")
@@ -1,21 +1,20 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: functions-framework
3
- Version: 3.5.0
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 :: 3 - Alpha
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
 
@@ -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
@@ -2,6 +2,7 @@ flask<4.0,>=1.0
2
2
  click<9.0,>=7.0
3
3
  watchdog>=1.0.0
4
4
  cloudevents<2.0.0,>=1.2.0
5
+ Werkzeug<4.0.0,>=0.14
5
6
 
6
7
  [:platform_system != "Windows"]
7
8
  gunicorn>=19.2.0