Skip to content

Commit 4c57790

Browse files
authored
develop ff-python based on ofn v1beta2 (#3)
* develop ff-python based on ofn v1beta2 Signed-off-by: laminar <[email protected]> * add exception_handler and logger Signed-off-by: laminar <[email protected]> * add user_context Signed-off-by: laminar <[email protected]> * adjust class name Signed-off-by: laminar <[email protected]> * add license header Signed-off-by: laminar <[email protected]> * Add check for function signature Signed-off-by: laminar <[email protected]> * adjust http trigger Signed-off-by: laminar <[email protected]> * fix Signed-off-by: laminar <[email protected]> * fix Signed-off-by: laminar <[email protected]> --------- Signed-off-by: laminar <[email protected]>
1 parent c011260 commit 4c57790

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+903
-1151
lines changed

Diff for: setup.cfg

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ line_length = 88
77
lines_between_types = 1
88
combine_as_imports = True
99
default_section = THIRDPARTY
10-
known_first_party = functions_framework, google.cloud.functions
10+
known_first_party = functions_framework

Diff for: setup.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,19 @@
4646
],
4747
keywords="functions-framework",
4848
packages=find_packages(where="src"),
49-
namespace_packages=["google", "google.cloud"],
5049
package_dir={"": "src"},
5150
python_requires=">=3.5, <4",
5251
install_requires=[
52+
"grpcio==1.54.2",
5353
"flask>=1.0,<3.0",
5454
"click>=7.0,<9.0",
55-
"watchdog>=1.0.0,<2.0.0",
55+
"uvicorn>=0.22.0",
5656
"gunicorn>=19.2.0,<21.0; platform_system!='Windows'",
5757
"cloudevents>=1.2.0,<2.0.0",
58-
"dapr>=1.6.0",
58+
"dapr>=1.10.0",
59+
"aiohttp==3.8.4",
60+
"dapr-ext-grpc>=1.10.0",
61+
"dapr-ext-fastapi>=1.10.0"
5962
],
6063
entry_points={
6164
"console_scripts": [

Diff for: src/functions_framework/__init__.py

+1-357
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2020 Google LLC
1+
# Copyright 2023 The OpenFunction Authors.
22
#
33
# Licensed under the Apache License, Version 2.0 (the "License");
44
# you may not use this file except in compliance with the License.
@@ -11,359 +11,3 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14-
15-
import functools
16-
import io
17-
import json
18-
import logging
19-
import os.path
20-
import pathlib
21-
import sys
22-
23-
import cloudevents.exceptions as cloud_exceptions
24-
import flask
25-
import werkzeug
26-
27-
from cloudevents.http import from_http, is_binary
28-
29-
from functions_framework import _function_registry, event_conversion
30-
from functions_framework.background_event import BackgroundEvent
31-
from functions_framework.exceptions import (
32-
EventConversionException,
33-
FunctionsFrameworkException,
34-
MissingSourceException,
35-
)
36-
from google.cloud.functions.context import Context
37-
from openfunction.dapr_output_middleware import dapr_output_middleware
38-
from openfunction.async_server import AsyncApp
39-
40-
MAX_CONTENT_LENGTH = 10 * 1024 * 1024
41-
42-
_FUNCTION_STATUS_HEADER_FIELD = "X-Google-Status"
43-
_CRASH = "crash"
44-
45-
_CLOUDEVENT_MIME_TYPE = "application/cloudevents+json"
46-
47-
48-
class _LoggingHandler(io.TextIOWrapper):
49-
"""Logging replacement for stdout and stderr in GCF Python 3.7."""
50-
51-
def __init__(self, level, stderr=sys.stderr):
52-
io.TextIOWrapper.__init__(self, io.StringIO(), encoding=stderr.encoding)
53-
self.level = level
54-
self.stderr = stderr
55-
56-
def write(self, out):
57-
payload = dict(severity=self.level, message=out.rstrip("\n"))
58-
return self.stderr.write(json.dumps(payload) + "\n")
59-
60-
61-
def cloud_event(func):
62-
"""Decorator that registers cloudevent as user function signature type."""
63-
_function_registry.REGISTRY_MAP[
64-
func.__name__
65-
] = _function_registry.CLOUDEVENT_SIGNATURE_TYPE
66-
67-
@functools.wraps(func)
68-
def wrapper(*args, **kwargs):
69-
return func(*args, **kwargs)
70-
71-
return wrapper
72-
73-
74-
def http(func):
75-
"""Decorator that registers http as user function signature type."""
76-
_function_registry.REGISTRY_MAP[
77-
func.__name__
78-
] = _function_registry.HTTP_SIGNATURE_TYPE
79-
80-
@functools.wraps(func)
81-
def wrapper(*args, **kwargs):
82-
return func(*args, **kwargs)
83-
84-
return wrapper
85-
86-
87-
def setup_logging():
88-
logging.getLogger().setLevel(logging.INFO)
89-
info_handler = logging.StreamHandler(sys.stdout)
90-
info_handler.setLevel(logging.NOTSET)
91-
info_handler.addFilter(lambda record: record.levelno <= logging.INFO)
92-
logging.getLogger().addHandler(info_handler)
93-
94-
warn_handler = logging.StreamHandler(sys.stderr)
95-
warn_handler.setLevel(logging.WARNING)
96-
logging.getLogger().addHandler(warn_handler)
97-
98-
99-
def setup_logging_level(debug):
100-
if debug:
101-
logging.getLogger().setLevel(logging.DEBUG)
102-
103-
104-
def _http_view_func_wrapper(function, request):
105-
def view_func(path):
106-
return function(request._get_current_object())
107-
108-
return view_func
109-
110-
111-
def _run_cloud_event(function, request):
112-
data = request.get_data()
113-
event = from_http(request.headers, data)
114-
function(event)
115-
116-
117-
def _cloud_event_view_func_wrapper(function, request):
118-
def view_func(path):
119-
ce_exception = None
120-
event = None
121-
try:
122-
event = from_http(request.headers, request.get_data())
123-
except (
124-
cloud_exceptions.MissingRequiredFields,
125-
cloud_exceptions.InvalidRequiredFields,
126-
) as e:
127-
ce_exception = e
128-
129-
if not ce_exception:
130-
function(event)
131-
return "OK"
132-
133-
# Not a CloudEvent. Try converting to a CloudEvent.
134-
try:
135-
function(event_conversion.background_event_to_cloud_event(request))
136-
except EventConversionException as e:
137-
flask.abort(
138-
400,
139-
description=(
140-
"Function was defined with FUNCTION_SIGNATURE_TYPE=cloudevent but"
141-
" parsing CloudEvent failed and converting from background event to"
142-
f" CloudEvent also failed.\nGot HTTP headers: {request.headers}\nGot"
143-
f" data: {request.get_data()}\nGot CloudEvent exception: {repr(ce_exception)}"
144-
f"\nGot background event conversion exception: {repr(e)}"
145-
),
146-
)
147-
return "OK"
148-
149-
return view_func
150-
151-
152-
def _event_view_func_wrapper(function, request):
153-
def view_func(path):
154-
if event_conversion.is_convertable_cloud_event(request):
155-
# Convert this CloudEvent to the equivalent background event data and context.
156-
data, context = event_conversion.cloud_event_to_background_event(request)
157-
function(data, context)
158-
elif is_binary(request.headers):
159-
# Support CloudEvents in binary content mode, with data being the
160-
# whole request body and context attributes retrieved from request
161-
# headers.
162-
data = request.get_data()
163-
context = Context(
164-
eventId=request.headers.get("ce-eventId"),
165-
timestamp=request.headers.get("ce-timestamp"),
166-
eventType=request.headers.get("ce-eventType"),
167-
resource=request.headers.get("ce-resource"),
168-
)
169-
function(data, context)
170-
else:
171-
# This is a regular CloudEvent
172-
event_data = event_conversion.marshal_background_event_data(request)
173-
if not event_data:
174-
flask.abort(400)
175-
event_object = BackgroundEvent(**event_data)
176-
data = event_object.data
177-
context = Context(**event_object.context)
178-
function(data, context)
179-
180-
return "OK"
181-
182-
return view_func
183-
184-
185-
def _configure_app(app, function, signature_type, func_context):
186-
# Mount the function at the root. Support GCF's default path behavior
187-
# Modify the url_map and view_functions directly here instead of using
188-
# add_url_rule in order to create endpoints that route all methods
189-
if signature_type == _function_registry.HTTP_SIGNATURE_TYPE:
190-
app.url_map.add(
191-
werkzeug.routing.Rule("/", defaults={"path": ""}, endpoint="run")
192-
)
193-
app.url_map.add(werkzeug.routing.Rule("/robots.txt", endpoint="error"))
194-
app.url_map.add(werkzeug.routing.Rule("/favicon.ico", endpoint="error"))
195-
app.url_map.add(werkzeug.routing.Rule("/<path:path>", endpoint="run"))
196-
app.view_functions["run"] = _http_view_func_wrapper(function, flask.request)
197-
app.view_functions["error"] = lambda: flask.abort(404, description="Not Found")
198-
app.after_request(read_request)
199-
app.after_request(dapr_output_middleware(func_context))
200-
elif signature_type == _function_registry.BACKGROUNDEVENT_SIGNATURE_TYPE:
201-
app.url_map.add(
202-
werkzeug.routing.Rule(
203-
"/", defaults={"path": ""}, endpoint="run", methods=["POST"]
204-
)
205-
)
206-
app.url_map.add(
207-
werkzeug.routing.Rule("/<path:path>", endpoint="run", methods=["POST"])
208-
)
209-
app.view_functions["run"] = _event_view_func_wrapper(function, flask.request)
210-
# Add a dummy endpoint for GET /
211-
app.url_map.add(werkzeug.routing.Rule("/", endpoint="get", methods=["GET"]))
212-
app.view_functions["get"] = lambda: ""
213-
elif signature_type == _function_registry.CLOUDEVENT_SIGNATURE_TYPE:
214-
app.url_map.add(
215-
werkzeug.routing.Rule(
216-
"/", defaults={"path": ""}, endpoint=signature_type, methods=["POST"]
217-
)
218-
)
219-
app.url_map.add(
220-
werkzeug.routing.Rule(
221-
"/<path:path>", endpoint=signature_type, methods=["POST"]
222-
)
223-
)
224-
225-
app.view_functions[signature_type] = _cloud_event_view_func_wrapper(
226-
function, flask.request
227-
)
228-
else:
229-
raise FunctionsFrameworkException(
230-
"Invalid signature type: {signature_type}".format(
231-
signature_type=signature_type
232-
)
233-
)
234-
235-
236-
def read_request(response):
237-
"""
238-
Force the framework to read the entire request before responding, to avoid
239-
connection errors when returning prematurely.
240-
"""
241-
242-
flask.request.get_data()
243-
return response
244-
245-
246-
def crash_handler(e):
247-
"""
248-
Return crash header to allow logging 'crash' message in logs.
249-
"""
250-
return str(e), 500, {_FUNCTION_STATUS_HEADER_FIELD: _CRASH}
251-
252-
def create_async_app(target=None, source=None, func_context=None, debug=False):
253-
target = _function_registry.get_function_target(target)
254-
source = _function_registry.get_function_source(source)
255-
256-
if not os.path.exists(source):
257-
raise MissingSourceException(
258-
"File {source} that is expected to define function doesn't exist".format(
259-
source=source
260-
)
261-
)
262-
263-
source_module, spec = _function_registry.load_function_module(source)
264-
spec.loader.exec_module(source_module)
265-
266-
function = _function_registry.get_user_function(source, source_module, target)
267-
268-
setup_logging_level(debug)
269-
270-
async_app = AsyncApp(func_context)
271-
async_app.bind(function)
272-
273-
return async_app.app
274-
275-
276-
def create_app(target=None, source=None, signature_type=None, func_context=None, debug=False):
277-
target = _function_registry.get_function_target(target)
278-
source = _function_registry.get_function_source(source)
279-
280-
# Set the template folder relative to the source path
281-
# Python 3.5: join does not support PosixPath
282-
template_folder = str(pathlib.Path(source).parent / "templates")
283-
284-
if not os.path.exists(source):
285-
raise MissingSourceException(
286-
"File {source} that is expected to define function doesn't exist".format(
287-
source=source
288-
)
289-
)
290-
291-
source_module, spec = _function_registry.load_function_module(source)
292-
293-
# Create the application
294-
_app = flask.Flask(target, template_folder=template_folder)
295-
_app.config["MAX_CONTENT_LENGTH"] = MAX_CONTENT_LENGTH
296-
_app.register_error_handler(500, crash_handler)
297-
global errorhandler
298-
errorhandler = _app.errorhandler
299-
300-
# Handle legacy GCF Python 3.7 behavior
301-
if os.environ.get("ENTRY_POINT"):
302-
os.environ["FUNCTION_NAME"] = os.environ.get("K_SERVICE", target)
303-
_app.make_response_original = _app.make_response
304-
305-
def handle_none(rv):
306-
if rv is None:
307-
rv = "OK"
308-
return _app.make_response_original(rv)
309-
310-
_app.make_response = handle_none
311-
312-
# Handle log severity backwards compatibility
313-
sys.stdout = _LoggingHandler("INFO", sys.stderr)
314-
sys.stderr = _LoggingHandler("ERROR", sys.stderr)
315-
setup_logging()
316-
317-
setup_logging_level(debug)
318-
319-
# Execute the module, within the application context
320-
with _app.app_context():
321-
spec.loader.exec_module(source_module)
322-
323-
# Get the configured function signature type
324-
signature_type = _function_registry.get_func_signature_type(target, signature_type)
325-
function = _function_registry.get_user_function(source, source_module, target)
326-
327-
_configure_app(_app, function, signature_type, func_context)
328-
329-
return _app
330-
331-
332-
class LazyWSGIApp:
333-
"""
334-
Wrap the WSGI app in a lazily initialized wrapper to prevent initialization
335-
at import-time
336-
"""
337-
338-
def __init__(self, target=None, source=None, signature_type=None, func_context=None, debug=False):
339-
# Support HTTP frameworks which support WSGI callables.
340-
# Note: this ability is currently broken in Gunicorn 20.0, and
341-
# environment variables should be used for configuration instead:
342-
# https://github.com/benoitc/gunicorn/issues/2159
343-
self.target = target
344-
self.source = source
345-
self.signature_type = signature_type
346-
self.func_context = func_context
347-
self.debug = debug
348-
349-
# Placeholder for the app which will be initialized on first call
350-
self.app = None
351-
352-
def __call__(self, *args, **kwargs):
353-
if not self.app:
354-
self.app = create_app(self.target, self.source, self.signature_type, self.func_context, self.debug)
355-
return self.app(*args, **kwargs)
356-
357-
358-
app = LazyWSGIApp()
359-
360-
361-
class DummyErrorHandler:
362-
def __init__(self):
363-
pass
364-
365-
def __call__(self, *args, **kwargs):
366-
return self
367-
368-
369-
errorhandler = DummyErrorHandler()

0 commit comments

Comments
 (0)