From 7780b58a8462e8c2ef06465a364429f8a6c6ef2e Mon Sep 17 00:00:00 2001 From: Pratiksha Kap Date: Fri, 21 Oct 2022 08:16:22 -0700 Subject: [PATCH 01/16] testing 1p --- src/functions_framework/__init__.py | 76 ++++++++++++++++++- src/functions_framework/_cli.py | 2 +- src/functions_framework/_function_registry.py | 8 ++ src/functions_framework/event_conversion.py | 4 + src/functions_framework/firstparty_event.py | 23 ++++++ 5 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 src/functions_framework/firstparty_event.py diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index 5d18d2ab..2b3bb998 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -19,6 +19,7 @@ import os.path import pathlib import sys +from typing import Type import cloudevents.exceptions as cloud_exceptions import flask @@ -33,7 +34,12 @@ FunctionsFrameworkException, MissingSourceException, ) +from functions_framework.firstparty_event import FirstPartyEvent from google.cloud.functions.context import Context +import sys + # caution: path[0] is reserved for script path (or '' in REPL) +sys.path.insert(1, '/usr/local/google/home/pratikshakap/code/sample/BigQuery/') + MAX_CONTENT_LENGTH = 10 * 1024 * 1024 @@ -55,13 +61,38 @@ def write(self, out): payload = dict(severity=self.level, message=out.rstrip("\n")) return self.stderr.write(json.dumps(payload) + "\n") - def cloud_event(func): + print("cloud_event(func)") """Decorator that registers cloudevent as user function signature type.""" _function_registry.REGISTRY_MAP[ func.__name__ ] = _function_registry.CLOUDEVENT_SIGNATURE_TYPE + @functools.wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + return wrapper +def input_type(googleType:Type): + def decorator(func): + _function_registry.REGISTRY_MAP[ + func.__name__ + ] = _function_registry.FIRSTPARTY_SIGNATURE_TYPE + _function_registry.INPUT_MAP[ + func.__name__ + ] = googleType + @functools.wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + return wrapper + return decorator + +def first_party(func): + print("first_party(func)") + """Decorator that registers cloudevent as user function signature type.""" + _function_registry.REGISTRY_MAP[ + func.__name__ + ] = _function_registry.FIRSTPARTY_SIGNATURE_TYPE @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) @@ -70,6 +101,7 @@ def wrapper(*args, **kwargs): def http(func): + print("http(func)") """Decorator that registers http as user function signature type.""" _function_registry.REGISTRY_MAP[ func.__name__ @@ -95,6 +127,7 @@ def setup_logging(): def _http_view_func_wrapper(function, request): + print("_http_view_func_wrapper") @functools.wraps(function) def view_func(path): return function(request._get_current_object()) @@ -103,12 +136,30 @@ def view_func(path): def _run_cloud_event(function, request): + print("_run_cloud_event") data = request.get_data() event = from_http(request.headers, data) function(event) +def _custom_event_func_wrapper(function, request,t:Type): + print("_custom_event_func_wrapper") + def view_func(path): + ce_exception = None + #event = from_http(request.headers, request.get_data()) + event_data = request.get_json() + event_object = FirstPartyEvent(event_data) + data = event_object.data + #context = Context(**event_object.context) + print(t) + + bqr= t(**data) + return function(bqr) + return "OK" + + return view_func def _cloud_event_view_func_wrapper(function, request): + print("_cloud_event_view_func_wrapper") def view_func(path): ce_exception = None event = None @@ -144,6 +195,7 @@ def view_func(path): def _event_view_func_wrapper(function, request): + print("_event_view_func_wrapper") def view_func(path): if event_conversion.is_convertable_cloud_event(request): # Convert this CloudEvent to the equivalent background event data and context. @@ -176,7 +228,8 @@ def view_func(path): return view_func -def _configure_app(app, function, signature_type): +def _configure_app(app, function, signature_type, inputType): + print("_configure_app") # Mount the function at the root. Support GCF's default path behavior # Modify the url_map and view_functions directly here instead of using # add_url_rule in order to create endpoints that route all methods @@ -218,6 +271,20 @@ def _configure_app(app, function, signature_type): app.view_functions[signature_type] = _cloud_event_view_func_wrapper( function, flask.request ) + elif signature_type == _function_registry.FIRSTPARTY_SIGNATURE_TYPE: + app.url_map.add( + werkzeug.routing.Rule( + "/", defaults={"path": ""}, endpoint=signature_type, methods=["POST"] + ) + ) + app.url_map.add( + werkzeug.routing.Rule( + "/", endpoint=signature_type, methods=["POST"] + ) + ) + app.view_functions[signature_type] = _custom_event_func_wrapper( + function, flask.request, inputType + ) else: raise FunctionsFrameworkException( "Invalid signature type: {signature_type}".format( @@ -227,6 +294,7 @@ def _configure_app(app, function, signature_type): def read_request(response): + print("read_request") """ Force the framework to read the entire request before responding, to avoid connection errors when returning prematurely. @@ -244,6 +312,7 @@ def crash_handler(e): def create_app(target=None, source=None, signature_type=None): + print("create_app") target = _function_registry.get_function_target(target) source = _function_registry.get_function_source(source) @@ -291,8 +360,9 @@ def handle_none(rv): # Get the configured function signature type signature_type = _function_registry.get_func_signature_type(target, signature_type) function = _function_registry.get_user_function(source, source_module, target) + inputType = _function_registry.get_func_input_type(target) - _configure_app(_app, function, signature_type) + _configure_app(_app, function, signature_type,inputType) return _app diff --git a/src/functions_framework/_cli.py b/src/functions_framework/_cli.py index 663ea50f..2aa3e733 100644 --- a/src/functions_framework/_cli.py +++ b/src/functions_framework/_cli.py @@ -26,7 +26,7 @@ @click.option( "--signature-type", envvar="FUNCTION_SIGNATURE_TYPE", - type=click.Choice(["http", "event", "cloudevent"]), + type=click.Choice(["http", "event", "cloudevent", "firstparty"]), default="http", ) @click.option("--host", envvar="HOST", type=click.STRING, default="0.0.0.0") diff --git a/src/functions_framework/_function_registry.py b/src/functions_framework/_function_registry.py index fdcf383f..a46f73b3 100644 --- a/src/functions_framework/_function_registry.py +++ b/src/functions_framework/_function_registry.py @@ -13,8 +13,10 @@ # limitations under the License. import importlib.util import os +from re import T import sys import types +from typing import Type from functions_framework.exceptions import ( InvalidConfigurationException, @@ -28,10 +30,12 @@ HTTP_SIGNATURE_TYPE = "http" CLOUDEVENT_SIGNATURE_TYPE = "cloudevent" BACKGROUNDEVENT_SIGNATURE_TYPE = "event" +FIRSTPARTY_SIGNATURE_TYPE = "firstparty" # REGISTRY_MAP stores the registered functions. # Keys are user function names, values are user function signature types. REGISTRY_MAP = {} +INPUT_MAP ={} def get_user_function(source, source_module, target): @@ -120,3 +124,7 @@ def get_func_signature_type(func_name: str, signature_type: str) -> str: if os.environ.get("ENTRY_POINT"): os.environ["FUNCTION_TRIGGER_TYPE"] = sig_type return sig_type + +def get_func_input_type(func_name: str) -> Type: + registered_type = INPUT_MAP[func_name] if func_name in REGISTRY_MAP else "" + return registered_type \ No newline at end of file diff --git a/src/functions_framework/event_conversion.py b/src/functions_framework/event_conversion.py index 06e5a812..458bcd7d 100644 --- a/src/functions_framework/event_conversion.py +++ b/src/functions_framework/event_conversion.py @@ -115,6 +115,7 @@ def background_event_to_cloud_event(request) -> CloudEvent: + print("background_event_to_cloud_event") """Converts a background event represented by the given HTTP request into a CloudEvent.""" event_data = marshal_background_event_data(request) if not event_data: @@ -185,6 +186,7 @@ def background_event_to_cloud_event(request) -> CloudEvent: def is_convertable_cloud_event(request) -> bool: + print("is_convertable_cloud_event") """Is the given request a known CloudEvent that can be converted to background event.""" if is_binary(request.headers): event_type = request.headers.get("ce-type") @@ -208,6 +210,7 @@ def _split_ce_source(source) -> Tuple[str, str]: def cloud_event_to_background_event(request) -> Tuple[Any, Context]: + print("cloud_event_to_background_event") """Converts a background event represented by the given HTTP request into a CloudEvent.""" try: event = from_http(request.headers, request.get_data()) @@ -294,6 +297,7 @@ def _split_resource(context: Context) -> Tuple[str, str, str]: def marshal_background_event_data(request): + print("marshal_background_event_data") """Marshal the request body of a raw Pub/Sub HTTP request into the schema that is expected of a background event""" try: diff --git a/src/functions_framework/firstparty_event.py b/src/functions_framework/firstparty_event.py new file mode 100644 index 00000000..a9f0f75e --- /dev/null +++ b/src/functions_framework/firstparty_event.py @@ -0,0 +1,23 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +class FirstPartyEvent(object): + + # Supports v1beta1, v1beta2, and v1 event formats. + def __init__( + self, + data, + ): + self.data = data From 7b8128f0d6d6258eb22e58fc2d0a3afebe42a415 Mon Sep 17 00:00:00 2001 From: Pratiksha Kap Date: Tue, 25 Oct 2022 12:05:46 -0700 Subject: [PATCH 02/16] handle response --- src/functions_framework/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index 2b3bb998..9223b6a9 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -152,9 +152,10 @@ def view_func(path): #context = Context(**event_object.context) print(t) - bqr= t(**data) - return function(bqr) - return "OK" + bqr= t.from_dict(data) + response = function(bqr) + return json.dumps(response.to_dict()) + #return "OK" return view_func @@ -226,7 +227,7 @@ def view_func(path): return "OK" return view_func - + def _configure_app(app, function, signature_type, inputType): print("_configure_app") From aac3caef7219462af21f296f4ce65352b76b8bf5 Mon Sep 17 00:00:00 2001 From: Pratiksha Kap Date: Wed, 26 Oct 2022 13:16:33 -0700 Subject: [PATCH 03/16] handle exceptions --- src/functions_framework/__init__.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index 9223b6a9..49ae969d 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -144,17 +144,18 @@ def _run_cloud_event(function, request): def _custom_event_func_wrapper(function, request,t:Type): print("_custom_event_func_wrapper") def view_func(path): - ce_exception = None - #event = from_http(request.headers, request.get_data()) - event_data = request.get_json() - event_object = FirstPartyEvent(event_data) - data = event_object.data - #context = Context(**event_object.context) - print(t) - - bqr= t.from_dict(data) - response = function(bqr) - return json.dumps(response.to_dict()) + try: + event_data = request.get_json() + event_object = FirstPartyEvent(event_data) + data = event_object.data + #context = Context(**event_object.context) + print(t) + + bqr= t.from_dict(data) + response = function(bqr) + return json.dumps(response.to_dict()) + except Exception as e: + return json.dumps(e.to_dict()), e.error_code #return "OK" return view_func From e7d5cf3515e6eb24f6cb9932625b32d6e3c7d301 Mon Sep 17 00:00:00 2001 From: Pratiksha Kap Date: Wed, 26 Oct 2022 13:18:15 -0700 Subject: [PATCH 04/16] remove print statements --- src/functions_framework/__init__.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index 49ae969d..1c4c11e4 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -62,7 +62,6 @@ def write(self, out): return self.stderr.write(json.dumps(payload) + "\n") def cloud_event(func): - print("cloud_event(func)") """Decorator that registers cloudevent as user function signature type.""" _function_registry.REGISTRY_MAP[ func.__name__ @@ -88,7 +87,6 @@ def wrapper(*args, **kwargs): return decorator def first_party(func): - print("first_party(func)") """Decorator that registers cloudevent as user function signature type.""" _function_registry.REGISTRY_MAP[ func.__name__ @@ -101,7 +99,6 @@ def wrapper(*args, **kwargs): def http(func): - print("http(func)") """Decorator that registers http as user function signature type.""" _function_registry.REGISTRY_MAP[ func.__name__ @@ -127,7 +124,6 @@ def setup_logging(): def _http_view_func_wrapper(function, request): - print("_http_view_func_wrapper") @functools.wraps(function) def view_func(path): return function(request._get_current_object()) @@ -136,20 +132,17 @@ def view_func(path): def _run_cloud_event(function, request): - print("_run_cloud_event") data = request.get_data() event = from_http(request.headers, data) function(event) def _custom_event_func_wrapper(function, request,t:Type): - print("_custom_event_func_wrapper") def view_func(path): try: event_data = request.get_json() event_object = FirstPartyEvent(event_data) data = event_object.data #context = Context(**event_object.context) - print(t) bqr= t.from_dict(data) response = function(bqr) @@ -161,7 +154,6 @@ def view_func(path): return view_func def _cloud_event_view_func_wrapper(function, request): - print("_cloud_event_view_func_wrapper") def view_func(path): ce_exception = None event = None @@ -197,7 +189,6 @@ def view_func(path): def _event_view_func_wrapper(function, request): - print("_event_view_func_wrapper") def view_func(path): if event_conversion.is_convertable_cloud_event(request): # Convert this CloudEvent to the equivalent background event data and context. @@ -231,7 +222,6 @@ def view_func(path): def _configure_app(app, function, signature_type, inputType): - print("_configure_app") # Mount the function at the root. Support GCF's default path behavior # Modify the url_map and view_functions directly here instead of using # add_url_rule in order to create endpoints that route all methods @@ -296,7 +286,6 @@ def _configure_app(app, function, signature_type, inputType): def read_request(response): - print("read_request") """ Force the framework to read the entire request before responding, to avoid connection errors when returning prematurely. @@ -314,7 +303,6 @@ def crash_handler(e): def create_app(target=None, source=None, signature_type=None): - print("create_app") target = _function_registry.get_function_target(target) source = _function_registry.get_function_source(source) From 8a26b3340907c3f1739bf5137b660a8f2328782c Mon Sep 17 00:00:00 2001 From: Pratiksha Kap Date: Wed, 9 Nov 2022 11:52:09 -0800 Subject: [PATCH 05/16] some changes --- src/functions_framework/__init__.py | 47 +++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index 1c4c11e4..cf02c5ad 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -13,12 +13,15 @@ # limitations under the License. import functools +from inspect import signature +import inspect import io import json import logging import os.path import pathlib import sys +import types from typing import Type import cloudevents.exceptions as cloud_exceptions @@ -37,9 +40,14 @@ from functions_framework.firstparty_event import FirstPartyEvent from google.cloud.functions.context import Context import sys - # caution: path[0] is reserved for script path (or '' in REPL) -sys.path.insert(1, '/usr/local/google/home/pratikshakap/code/sample/BigQuery/') +import os +#sys.path.insert(1, os.path.abspath('../../../sample/python/BigQueryFolder/')) + # caution: path[0] is reserved for script path (or '' in REPL) +sys.path.insert(1, '/usr/local/google/home/pratikshakap/code/sample/BigQueryFolder/') +#sys.path.insert(2, '..') +import BigQuery +#import BigQueryFolder MAX_CONTENT_LENGTH = 10 * 1024 * 1024 @@ -72,19 +80,36 @@ def wrapper(*args, **kwargs): return wrapper -def input_type(googleType:Type): - def decorator(func): +def input_type(googleType): + print(googleType) + # no parameter to the decorator + if isinstance(googleType, types.FunctionType): + func = googleType + sig= signature(func) + _function_registry.INPUT_MAP[ + func.__name__ + ] = list(sig.parameters.values())[0].annotation _function_registry.REGISTRY_MAP[ func.__name__ ] = _function_registry.FIRSTPARTY_SIGNATURE_TYPE - _function_registry.INPUT_MAP[ - func.__name__ - ] = googleType @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper - return decorator + # type parameter provided to the decorator + else: + def decorator(func): + _function_registry.INPUT_MAP[ + func.__name__ + ] = googleType + _function_registry.REGISTRY_MAP[ + func.__name__ + ] = _function_registry.FIRSTPARTY_SIGNATURE_TYPE + @functools.wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + return wrapper + return decorator def first_party(func): """Decorator that registers cloudevent as user function signature type.""" @@ -139,13 +164,17 @@ def _run_cloud_event(function, request): def _custom_event_func_wrapper(function, request,t:Type): def view_func(path): try: + print(request.headers) + print(request) event_data = request.get_json() + print(event_data) event_object = FirstPartyEvent(event_data) data = event_object.data #context = Context(**event_object.context) - + print(type(t)) bqr= t.from_dict(data) response = function(bqr) + print(type(response)) return json.dumps(response.to_dict()) except Exception as e: return json.dumps(e.to_dict()), e.error_code From 9855468c3ddcf60030c0be1baf5f1f619db0ae26 Mon Sep 17 00:00:00 2001 From: Pratiksha Kap Date: Mon, 21 Nov 2022 11:15:22 -0800 Subject: [PATCH 06/16] push more 1p changes --- src/functions_framework/__init__.py | 58 +++++++------------ src/functions_framework/_cli.py | 2 +- src/functions_framework/_function_registry.py | 4 +- 3 files changed, 24 insertions(+), 40 deletions(-) diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index cf02c5ad..304ea81c 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -46,7 +46,7 @@ # caution: path[0] is reserved for script path (or '' in REPL) sys.path.insert(1, '/usr/local/google/home/pratikshakap/code/sample/BigQueryFolder/') #sys.path.insert(2, '..') -import BigQuery + #import BigQueryFolder MAX_CONTENT_LENGTH = 10 * 1024 * 1024 @@ -80,47 +80,35 @@ def wrapper(*args, **kwargs): return wrapper -def input_type(googleType): - print(googleType) +def typed(googleType): # no parameter to the decorator if isinstance(googleType, types.FunctionType): func = googleType - sig= signature(func) + sig = signature(func) _function_registry.INPUT_MAP[ func.__name__ ] = list(sig.parameters.values())[0].annotation _function_registry.REGISTRY_MAP[ func.__name__ - ] = _function_registry.FIRSTPARTY_SIGNATURE_TYPE + ] = _function_registry.TYPED_SIGNATURE_TYPE @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper # type parameter provided to the decorator else: - def decorator(func): + def func_decorator(func): _function_registry.INPUT_MAP[ func.__name__ ] = googleType _function_registry.REGISTRY_MAP[ func.__name__ - ] = _function_registry.FIRSTPARTY_SIGNATURE_TYPE + ] = _function_registry.TYPED_SIGNATURE_TYPE @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper - return decorator - -def first_party(func): - """Decorator that registers cloudevent as user function signature type.""" - _function_registry.REGISTRY_MAP[ - func.__name__ - ] = _function_registry.FIRSTPARTY_SIGNATURE_TYPE - @functools.wraps(func) - def wrapper(*args, **kwargs): - return func(*args, **kwargs) - - return wrapper + return func_decorator def http(func): @@ -161,24 +149,19 @@ def _run_cloud_event(function, request): event = from_http(request.headers, data) function(event) -def _custom_event_func_wrapper(function, request,t:Type): +def _custom_event_func_wrapper(function, request,inputType:Type): def view_func(path): - try: - print(request.headers) - print(request) - event_data = request.get_json() - print(event_data) - event_object = FirstPartyEvent(event_data) - data = event_object.data - #context = Context(**event_object.context) - print(type(t)) - bqr= t.from_dict(data) - response = function(bqr) - print(type(response)) - return json.dumps(response.to_dict()) - except Exception as e: - return json.dumps(e.to_dict()), e.error_code - #return "OK" + data = request.get_json() + if not (hasattr(inputType, 'from_dict') and callable(getattr(inputType, 'from_dict'))): + raise MissingSourceException( # pass the correct exception + "ABORTTTTTTT" + ) + input = inputType.from_dict(data) + response = function(input) + print(response.__class__.__module__) + if response.__class__.__module__== "builtins": + return response + return json.dumps(response.to_dict()) return view_func @@ -292,7 +275,8 @@ def _configure_app(app, function, signature_type, inputType): app.view_functions[signature_type] = _cloud_event_view_func_wrapper( function, flask.request ) - elif signature_type == _function_registry.FIRSTPARTY_SIGNATURE_TYPE: + elif signature_type == _function_registry.TYPED_SIGNATURE_TYPE: + #validitycheck() app.url_map.add( werkzeug.routing.Rule( "/", defaults={"path": ""}, endpoint=signature_type, methods=["POST"] diff --git a/src/functions_framework/_cli.py b/src/functions_framework/_cli.py index 2aa3e733..fc11f434 100644 --- a/src/functions_framework/_cli.py +++ b/src/functions_framework/_cli.py @@ -26,7 +26,7 @@ @click.option( "--signature-type", envvar="FUNCTION_SIGNATURE_TYPE", - type=click.Choice(["http", "event", "cloudevent", "firstparty"]), + type=click.Choice(["http", "event", "cloudevent", "typed"]), default="http", ) @click.option("--host", envvar="HOST", type=click.STRING, default="0.0.0.0") diff --git a/src/functions_framework/_function_registry.py b/src/functions_framework/_function_registry.py index a46f73b3..f72bf40e 100644 --- a/src/functions_framework/_function_registry.py +++ b/src/functions_framework/_function_registry.py @@ -30,7 +30,7 @@ HTTP_SIGNATURE_TYPE = "http" CLOUDEVENT_SIGNATURE_TYPE = "cloudevent" BACKGROUNDEVENT_SIGNATURE_TYPE = "event" -FIRSTPARTY_SIGNATURE_TYPE = "firstparty" +TYPED_SIGNATURE_TYPE = "typed" # REGISTRY_MAP stores the registered functions. # Keys are user function names, values are user function signature types. @@ -126,5 +126,5 @@ def get_func_signature_type(func_name: str, signature_type: str) -> str: return sig_type def get_func_input_type(func_name: str) -> Type: - registered_type = INPUT_MAP[func_name] if func_name in REGISTRY_MAP else "" + registered_type = INPUT_MAP[func_name] if func_name in INPUT_MAP else "" return registered_type \ No newline at end of file From 0c74e60e491cfcdf39ddfad4b9e9b2dfcf7cd1ae Mon Sep 17 00:00:00 2001 From: Pratiksha Kap Date: Tue, 22 Nov 2022 13:28:15 -0800 Subject: [PATCH 07/16] add exceptions --- src/functions_framework/__init__.py | 34 +++++++---- src/functions_framework/exceptions.py | 9 +++ src/functions_framework/firstparty_event.py | 23 -------- src/functions_framework/typed_event.py | 62 +++++++++++++++++++++ 4 files changed, 95 insertions(+), 33 deletions(-) delete mode 100644 src/functions_framework/firstparty_event.py create mode 100644 src/functions_framework/typed_event.py diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index 8d2b6c25..3e2f889d 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -30,14 +30,13 @@ from cloudevents.http import from_http, is_binary -from functions_framework import _function_registry, event_conversion +from functions_framework import _function_registry, event_conversion, typed_event from functions_framework.background_event import BackgroundEvent from functions_framework.exceptions import ( EventConversionException, FunctionsFrameworkException, MissingSourceException, ) -from functions_framework.firstparty_event import FirstPartyEvent from google.cloud.functions.context import Context import sys import os @@ -78,6 +77,25 @@ def wrapper(*args, **kwargs): return wrapper +def typednew(googleType): + # no parameter to the decorator + if isinstance(googleType, types.FunctionType): + func=googleType + typed_event.register_typed_event("", func) + @functools.wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + return wrapper + # type parameter provided to the decorator + else: + def func_decorator(func): + typed_event.register_typed_event(googleType, func) + @functools.wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + return wrapper + return func_decorator + def typed(googleType): # no parameter to the decorator if isinstance(googleType, types.FunctionType): @@ -147,18 +165,15 @@ def _run_cloud_event(function, request): event = from_http(request.headers, data) function(event) -def _custom_event_func_wrapper(function, request,inputType:Type): +def _typed_event_func_wrapper(function, request,inputType:Type): def view_func(path): data = request.get_json() - if not (hasattr(inputType, 'from_dict') and callable(getattr(inputType, 'from_dict'))): - raise MissingSourceException( # pass the correct exception - "ABORTTTTTTT" - ) input = inputType.from_dict(data) response = function(input) print(response.__class__.__module__) if response.__class__.__module__== "builtins": return response + typed_event.validate_return_type(response) return json.dumps(response.to_dict()) return view_func @@ -274,7 +289,7 @@ def _configure_app(app, function, signature_type, inputType): function, flask.request ) elif signature_type == _function_registry.TYPED_SIGNATURE_TYPE: - #validitycheck() + #validity_check() app.url_map.add( werkzeug.routing.Rule( "/", defaults={"path": ""}, endpoint=signature_type, methods=["POST"] @@ -285,7 +300,7 @@ def _configure_app(app, function, signature_type, inputType): "/", endpoint=signature_type, methods=["POST"] ) ) - app.view_functions[signature_type] = _custom_event_func_wrapper( + app.view_functions[signature_type] = _typed_event_func_wrapper( function, flask.request, inputType ) else: @@ -366,7 +381,6 @@ def handle_none(rv): return _app - class LazyWSGIApp: """ Wrap the WSGI app in a lazily initialized wrapper to prevent initialization diff --git a/src/functions_framework/exceptions.py b/src/functions_framework/exceptions.py index 671a28a4..ea207f08 100644 --- a/src/functions_framework/exceptions.py +++ b/src/functions_framework/exceptions.py @@ -35,3 +35,12 @@ class MissingTargetException(FunctionsFrameworkException): class EventConversionException(FunctionsFrameworkException): pass + +class MissingTypeException(FunctionsFrameworkException): + pass + +class TypeMismatchException(FunctionsFrameworkException): + pass + +class MissingMethodException(FunctionsFrameworkException): + pass \ No newline at end of file diff --git a/src/functions_framework/firstparty_event.py b/src/functions_framework/firstparty_event.py deleted file mode 100644 index a9f0f75e..00000000 --- a/src/functions_framework/firstparty_event.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2021 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -class FirstPartyEvent(object): - - # Supports v1beta1, v1beta2, and v1 event formats. - def __init__( - self, - data, - ): - self.data = data diff --git a/src/functions_framework/typed_event.py b/src/functions_framework/typed_event.py new file mode 100644 index 00000000..28325c6f --- /dev/null +++ b/src/functions_framework/typed_event.py @@ -0,0 +1,62 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from inspect import signature +import inspect + +from functions_framework.exceptions import MissingMethodException, MissingTypeException, TypeMismatchException +from functions_framework import _function_registry + +class TypedEvent(object): + + # Supports v1beta1, v1beta2, and v1 event formats. + def __init__( + self, + data, + ): + self.data = data + +def register_typed_event(decorator_type, func): + sig = signature(func) + annotation_type = list(sig.parameters.values())[0].annotation + if(decorator_type == "" and annotation_type is inspect._empty): + raise MissingTypeException("The function defined does not contain Type of the input object.") + + if(decorator_type != "" and annotation_type is not inspect._empty and decorator_type != annotation_type): + raise TypeMismatchException("The object type provided via 'typed' {decorator_type}" + "is different from the one in the function annotation {annotation_type}.".format( + decorator_type=decorator_type, annotation_type=annotation_type + )) + + if(decorator_type == ""): + decorator_type = annotation_type + + if not (hasattr(decorator_type, 'from_dict') and callable(getattr(decorator_type, 'from_dict'))): + raise MissingMethodException("The type {decorator_type} does not have the required method called " + " 'from_dict'.".format(decorator_type=decorator_type)) + + _function_registry.INPUT_MAP[ + func.__name__ + ] = decorator_type + _function_registry.REGISTRY_MAP[ + func.__name__ + ] = _function_registry.TYPED_SIGNATURE_TYPE + + print(f"#### {decorator_type=} &&& {annotation_type=}") + +def validate_return_type(response): + if not (hasattr(response, 'to_dict') and callable(getattr(response, 'to_dict'))): + raise MissingMethodException("The type {response} does not have the required method called " + " 'to_dict'.".format(response=response)) \ No newline at end of file From a7e26e8bcad0025dbe870d7e199c832aaaf52e09 Mon Sep 17 00:00:00 2001 From: Pratiksha Kap Date: Tue, 29 Nov 2022 11:35:04 -0800 Subject: [PATCH 08/16] add unit tests --- src/functions_framework/__init__.py | 37 ++-------- src/functions_framework/typed_event.py | 34 ++++++---- .../typed_events/missing_from_dict.py | 53 +++++++++++++++ .../typed_events/missing_to_dict.py | 55 +++++++++++++++ .../typed_events/missing_type.py | 28 ++++++++ .../typed_events/typed_event.py | 68 +++++++++++++++++++ tests/test_typed_event_functions.py | 64 +++++++++++++++++ 7 files changed, 291 insertions(+), 48 deletions(-) create mode 100644 tests/test_functions/typed_events/missing_from_dict.py create mode 100644 tests/test_functions/typed_events/missing_to_dict.py create mode 100644 tests/test_functions/typed_events/missing_type.py create mode 100644 tests/test_functions/typed_events/typed_event.py create mode 100644 tests/test_typed_event_functions.py diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index 3e2f889d..d1a6be6f 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -77,7 +77,7 @@ def wrapper(*args, **kwargs): return wrapper -def typednew(googleType): +def typed(googleType): # no parameter to the decorator if isinstance(googleType, types.FunctionType): func=googleType @@ -96,36 +96,6 @@ def wrapper(*args, **kwargs): return wrapper return func_decorator -def typed(googleType): - # no parameter to the decorator - if isinstance(googleType, types.FunctionType): - func = googleType - sig = signature(func) - _function_registry.INPUT_MAP[ - func.__name__ - ] = list(sig.parameters.values())[0].annotation - _function_registry.REGISTRY_MAP[ - func.__name__ - ] = _function_registry.TYPED_SIGNATURE_TYPE - @functools.wraps(func) - def wrapper(*args, **kwargs): - return func(*args, **kwargs) - return wrapper - # type parameter provided to the decorator - else: - def func_decorator(func): - _function_registry.INPUT_MAP[ - func.__name__ - ] = googleType - _function_registry.REGISTRY_MAP[ - func.__name__ - ] = _function_registry.TYPED_SIGNATURE_TYPE - @functools.wraps(func) - def wrapper(*args, **kwargs): - return func(*args, **kwargs) - return wrapper - return func_decorator - def http(func): """Decorator that registers http as user function signature type.""" @@ -170,11 +140,12 @@ def view_func(path): data = request.get_json() input = inputType.from_dict(data) response = function(input) - print(response.__class__.__module__) + if response is None: + return "OK" if response.__class__.__module__== "builtins": return response typed_event.validate_return_type(response) - return json.dumps(response.to_dict()) + return response.to_dict() return view_func diff --git a/src/functions_framework/typed_event.py b/src/functions_framework/typed_event.py index 28325c6f..e0674aeb 100644 --- a/src/functions_framework/typed_event.py +++ b/src/functions_framework/typed_event.py @@ -31,6 +31,24 @@ def __init__( def register_typed_event(decorator_type, func): sig = signature(func) annotation_type = list(sig.parameters.values())[0].annotation + + type_validity_check(decorator_type, annotation_type) + if(decorator_type == ""): + decorator_type = annotation_type + + _function_registry.INPUT_MAP[ + func.__name__ + ] = decorator_type + _function_registry.REGISTRY_MAP[ + func.__name__ + ] = _function_registry.TYPED_SIGNATURE_TYPE + +def validate_return_type(response): + if not (hasattr(response, 'to_dict') and callable(getattr(response, 'to_dict'))): + raise MissingMethodException("The type {response} does not have the required method called " + " 'to_dict'.".format(response=response)) + +def type_validity_check(decorator_type, annotation_type): if(decorator_type == "" and annotation_type is inspect._empty): raise MissingTypeException("The function defined does not contain Type of the input object.") @@ -45,18 +63,4 @@ def register_typed_event(decorator_type, func): if not (hasattr(decorator_type, 'from_dict') and callable(getattr(decorator_type, 'from_dict'))): raise MissingMethodException("The type {decorator_type} does not have the required method called " - " 'from_dict'.".format(decorator_type=decorator_type)) - - _function_registry.INPUT_MAP[ - func.__name__ - ] = decorator_type - _function_registry.REGISTRY_MAP[ - func.__name__ - ] = _function_registry.TYPED_SIGNATURE_TYPE - - print(f"#### {decorator_type=} &&& {annotation_type=}") - -def validate_return_type(response): - if not (hasattr(response, 'to_dict') and callable(getattr(response, 'to_dict'))): - raise MissingMethodException("The type {response} does not have the required method called " - " 'to_dict'.".format(response=response)) \ No newline at end of file + " 'from_dict'.".format(decorator_type=decorator_type)) \ No newline at end of file diff --git a/tests/test_functions/typed_events/missing_from_dict.py b/tests/test_functions/typed_events/missing_from_dict.py new file mode 100644 index 00000000..e20180ba --- /dev/null +++ b/tests/test_functions/typed_events/missing_from_dict.py @@ -0,0 +1,53 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Function used to test handling functions using typed decorators.""" +import flask + +import functions_framework + +from typing import Any, TypeVar + +T = TypeVar("T") + +def from_str(x: Any) -> str: + assert isinstance(x, str) + return x + +def from_int(x: Any) -> int: + assert isinstance(x, int) and not isinstance(x, bool) + +class TestTypeMissingFromDict: + name: str + age: int + + def __init__(self, name: str, age: int) -> None: + self.name = name + self.age = age + + def to_dict(self) -> dict: + result: dict = {} + result["name"] = from_str(self.name) + result["age"] = from_int(self.age) + return result + +@functions_framework.typednew(TestTypeMissingFromDict) +def function_typed_missing_from_dict(testType:TestTypeMissingFromDict): + valid_event =( + testType.name == "john" + and testType.age == 10 + ) + if not valid_event: + flask.abort(500) + return testType \ No newline at end of file diff --git a/tests/test_functions/typed_events/missing_to_dict.py b/tests/test_functions/typed_events/missing_to_dict.py new file mode 100644 index 00000000..ee6a7b63 --- /dev/null +++ b/tests/test_functions/typed_events/missing_to_dict.py @@ -0,0 +1,55 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Function used to test handling functions using typed decorators.""" +import flask + +import functions_framework + +from typing import Any, TypeVar + +T = TypeVar("T") + +def from_str(x: Any) -> str: + assert isinstance(x, str) + return x + +def from_int(x: Any) -> int: + assert isinstance(x, int) and not isinstance(x, bool) + + +class TestTypeMissingToDict: + name: str + age: int + + def __init__(self, name: str, age: int) -> None: + self.name = name + self.age = age + + @staticmethod + def from_dict(obj: dict) -> 'TestTypeMissingToDict': + name = from_str(obj.get("name")) + age = from_int(obj.get("age")) + return TestTypeMissingToDict(name, age) + + +@functions_framework.typednew(TestTypeMissingToDict) +def function_typed_missing_to_dict(testType:TestTypeMissingToDict): + valid_event =( + testType.name == "john" + and testType.age == 10 + ) + if not valid_event: + flask.abort(500) + return testType \ No newline at end of file diff --git a/tests/test_functions/typed_events/missing_type.py b/tests/test_functions/typed_events/missing_type.py new file mode 100644 index 00000000..2bca56eb --- /dev/null +++ b/tests/test_functions/typed_events/missing_type.py @@ -0,0 +1,28 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Function used to test handling functions using typed decorators.""" +import flask + +import functions_framework + +@functions_framework.typednew +def function_typed_missing_type_information(testType): + valid_event =( + testType.name == "john" + and testType.age == 10 + ) + if not valid_event: + flask.abort(500) + return testType \ No newline at end of file diff --git a/tests/test_functions/typed_events/typed_event.py b/tests/test_functions/typed_events/typed_event.py new file mode 100644 index 00000000..13add756 --- /dev/null +++ b/tests/test_functions/typed_events/typed_event.py @@ -0,0 +1,68 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Function used to test handling functions using typed decorators.""" +import flask + +import functions_framework + +from typing import Any, TypeVar, Type, cast + +T = TypeVar("T") + +def from_str(x: Any) -> str: + assert isinstance(x, str) + return x + +def from_int(x: Any) -> int: + assert isinstance(x, int) and not isinstance(x, bool) + return x + +def to_class(c: Type[T], x: Any) -> dict: + assert isinstance(x, c) + return cast(Any, x).to_dict() + +class TestType: + name: str + age: int + + def __init__(self, name: str, age: int) -> None: + self.name = name + self.age = age + + @staticmethod + def from_dict(obj: dict) -> 'TestType': + name = from_str(obj.get("name")) + age = from_int(obj.get("age")) + return TestType(name, age) + + def to_dict(self) -> dict: + result: dict = {} + result["name"] = from_str(self.name) + result["age"] = from_int(self.age) + return result + + + + +@functions_framework.typednew(TestType) +def function_typed(testType:TestType): + valid_event =( + testType.name == "john" + and testType.age == 10 + ) + if not valid_event: + flask.abort(500) + return testType + diff --git a/tests/test_typed_event_functions.py b/tests/test_typed_event_functions.py new file mode 100644 index 00000000..5ab1cdda --- /dev/null +++ b/tests/test_typed_event_functions.py @@ -0,0 +1,64 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import pathlib + +import pytest + + +from functions_framework import create_app +from functions_framework.exceptions import MissingMethodException, MissingTypeException + +TEST_FUNCTIONS_DIR = pathlib.Path(__file__).resolve().parent / "test_functions" + +# Python 3.5: ModuleNotFoundError does not exist +try: + _ModuleNotFoundError = ModuleNotFoundError +except: + _ModuleNotFoundError = ImportError + + +@pytest.fixture +def typed_decorator_client(): + source = TEST_FUNCTIONS_DIR / "typed_events" / "typed_event.py" + target = "function_typed" + return create_app(target, source).test_client() + +@pytest.fixture +def typed_decorator_missing_todict(): + source = TEST_FUNCTIONS_DIR / "typed_events" / "missing_to_dict.py" + target = "function_typed_missing_to_dict" + return create_app(target, source).test_client() + +def test_typed_decorator(typed_decorator_client): + resp = typed_decorator_client.post("/", json={"name": "john" , "age": 10}) + assert resp.status_code == 200 + assert resp.data == b"{\"age\":10,\"name\":\"john\"}\n" + + +def test_missing_from_dict_typed_decorator(): + source = TEST_FUNCTIONS_DIR / "typed_events" / "missing_from_dict.py" + target = "function_typed_missing_from_dict" + with pytest.raises(MissingMethodException) as excinfo: + create_app(target, source).test_client() + + +def test_missing_type_information_typed_decorator(): + source = TEST_FUNCTIONS_DIR / "typed_events" / "missing_type.py" + target = "function_typed_missing_type_information" + with pytest.raises(MissingTypeException): + create_app(target, source).test_client() + +def test_missing_to_dict_typed_decorator(typed_decorator_missing_todict): + resp = typed_decorator_missing_todict.post("/", json={"name": "john" , "age": 10}) + assert resp.status_code == 500 From 0108b3135ed7ca9f0d5f5b0764c654b7059a2a81 Mon Sep 17 00:00:00 2001 From: Pratiksha Kap Date: Tue, 29 Nov 2022 12:59:51 -0800 Subject: [PATCH 09/16] lint fix --- src/functions_framework/__init__.py | 35 +++++----- src/functions_framework/_function_registry.py | 5 +- src/functions_framework/exceptions.py | 5 +- src/functions_framework/typed_event.py | 67 ++++++++++++------- .../typed_events/missing_from_dict.py | 19 +++--- .../typed_events/missing_to_dict.py | 18 ++--- .../typed_events/missing_type.py | 8 +-- .../typed_events/typed_event.py | 16 ++--- tests/test_typed_event_functions.py | 9 ++- 9 files changed, 105 insertions(+), 77 deletions(-) diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index d1a6be6f..c70e1aa6 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -38,15 +38,6 @@ MissingSourceException, ) from google.cloud.functions.context import Context -import sys -import os - -#sys.path.insert(1, os.path.abspath('../../../sample/python/BigQueryFolder/')) - # caution: path[0] is reserved for script path (or '' in REPL) -sys.path.insert(1, '/usr/local/google/home/pratikshakap/code/sample/BigQueryFolder/') -#sys.path.insert(2, '..') - -#import BigQueryFolder _FUNCTION_STATUS_HEADER_FIELD = "X-Google-Status" _CRASH = "crash" @@ -66,34 +57,43 @@ def write(self, out): payload = dict(severity=self.level, message=out.rstrip("\n")) return self.stderr.write(json.dumps(payload) + "\n") + def cloud_event(func): """Decorator that registers cloudevent as user function signature type.""" _function_registry.REGISTRY_MAP[ func.__name__ ] = _function_registry.CLOUDEVENT_SIGNATURE_TYPE + @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper + def typed(googleType): # no parameter to the decorator if isinstance(googleType, types.FunctionType): - func=googleType + func = googleType typed_event.register_typed_event("", func) + @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) + return wrapper # type parameter provided to the decorator - else: + else: + def func_decorator(func): typed_event.register_typed_event(googleType, func) + @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) + return wrapper + return func_decorator @@ -135,20 +135,22 @@ def _run_cloud_event(function, request): event = from_http(request.headers, data) function(event) -def _typed_event_func_wrapper(function, request,inputType:Type): + +def _typed_event_func_wrapper(function, request, inputType: Type): def view_func(path): data = request.get_json() input = inputType.from_dict(data) response = function(input) if response is None: return "OK" - if response.__class__.__module__== "builtins": + if response.__class__.__module__ == "builtins": return response typed_event.validate_return_type(response) return response.to_dict() return view_func + def _cloud_event_view_func_wrapper(function, request): def view_func(path): ce_exception = None @@ -215,7 +217,7 @@ def view_func(path): return "OK" return view_func - + def _configure_app(app, function, signature_type, inputType): # Mount the function at the root. Support GCF's default path behavior @@ -260,7 +262,7 @@ def _configure_app(app, function, signature_type, inputType): function, flask.request ) elif signature_type == _function_registry.TYPED_SIGNATURE_TYPE: - #validity_check() + # validity_check() app.url_map.add( werkzeug.routing.Rule( "/", defaults={"path": ""}, endpoint=signature_type, methods=["POST"] @@ -348,10 +350,11 @@ def handle_none(rv): function = _function_registry.get_user_function(source, source_module, target) inputType = _function_registry.get_func_input_type(target) - _configure_app(_app, function, signature_type,inputType) + _configure_app(_app, function, signature_type, inputType) return _app + class LazyWSGIApp: """ Wrap the WSGI app in a lazily initialized wrapper to prevent initialization diff --git a/src/functions_framework/_function_registry.py b/src/functions_framework/_function_registry.py index f72bf40e..102410b9 100644 --- a/src/functions_framework/_function_registry.py +++ b/src/functions_framework/_function_registry.py @@ -35,7 +35,7 @@ # REGISTRY_MAP stores the registered functions. # Keys are user function names, values are user function signature types. REGISTRY_MAP = {} -INPUT_MAP ={} +INPUT_MAP = {} def get_user_function(source, source_module, target): @@ -125,6 +125,7 @@ def get_func_signature_type(func_name: str, signature_type: str) -> str: os.environ["FUNCTION_TRIGGER_TYPE"] = sig_type return sig_type + def get_func_input_type(func_name: str) -> Type: registered_type = INPUT_MAP[func_name] if func_name in INPUT_MAP else "" - return registered_type \ No newline at end of file + return registered_type diff --git a/src/functions_framework/exceptions.py b/src/functions_framework/exceptions.py index ea207f08..bc6da58f 100644 --- a/src/functions_framework/exceptions.py +++ b/src/functions_framework/exceptions.py @@ -36,11 +36,14 @@ class MissingTargetException(FunctionsFrameworkException): class EventConversionException(FunctionsFrameworkException): pass + class MissingTypeException(FunctionsFrameworkException): pass + class TypeMismatchException(FunctionsFrameworkException): pass + class MissingMethodException(FunctionsFrameworkException): - pass \ No newline at end of file + pass diff --git a/src/functions_framework/typed_event.py b/src/functions_framework/typed_event.py index e0674aeb..a7450c54 100644 --- a/src/functions_framework/typed_event.py +++ b/src/functions_framework/typed_event.py @@ -16,9 +16,14 @@ from inspect import signature import inspect -from functions_framework.exceptions import MissingMethodException, MissingTypeException, TypeMismatchException +from functions_framework.exceptions import ( + MissingMethodException, + MissingTypeException, + TypeMismatchException, +) from functions_framework import _function_registry + class TypedEvent(object): # Supports v1beta1, v1beta2, and v1 event formats. @@ -28,39 +33,55 @@ def __init__( ): self.data = data + def register_typed_event(decorator_type, func): sig = signature(func) annotation_type = list(sig.parameters.values())[0].annotation - + type_validity_check(decorator_type, annotation_type) - if(decorator_type == ""): + if decorator_type == "": decorator_type = annotation_type - - _function_registry.INPUT_MAP[ - func.__name__ - ] = decorator_type + + _function_registry.INPUT_MAP[func.__name__] = decorator_type _function_registry.REGISTRY_MAP[ - func.__name__ + func.__name__ ] = _function_registry.TYPED_SIGNATURE_TYPE + def validate_return_type(response): - if not (hasattr(response, 'to_dict') and callable(getattr(response, 'to_dict'))): - raise MissingMethodException("The type {response} does not have the required method called " - " 'to_dict'.".format(response=response)) + if not (hasattr(response, "to_dict") and callable(getattr(response, "to_dict"))): + raise MissingMethodException( + "The type {response} does not have the required method called " + " 'to_dict'.".format(response=response) + ) + def type_validity_check(decorator_type, annotation_type): - if(decorator_type == "" and annotation_type is inspect._empty): - raise MissingTypeException("The function defined does not contain Type of the input object.") + if decorator_type == "" and annotation_type is inspect._empty: + raise MissingTypeException( + "The function defined does not contain Type of the input object." + ) - if(decorator_type != "" and annotation_type is not inspect._empty and decorator_type != annotation_type): - raise TypeMismatchException("The object type provided via 'typed' {decorator_type}" - "is different from the one in the function annotation {annotation_type}.".format( - decorator_type=decorator_type, annotation_type=annotation_type - )) + if ( + decorator_type != "" + and annotation_type is not inspect._empty + and decorator_type != annotation_type + ): + raise TypeMismatchException( + "The object type provided via 'typed' {decorator_type}" + "is different from the one in the function annotation {annotation_type}.".format( + decorator_type=decorator_type, annotation_type=annotation_type + ) + ) - if(decorator_type == ""): + if decorator_type == "": decorator_type = annotation_type - - if not (hasattr(decorator_type, 'from_dict') and callable(getattr(decorator_type, 'from_dict'))): - raise MissingMethodException("The type {decorator_type} does not have the required method called " - " 'from_dict'.".format(decorator_type=decorator_type)) \ No newline at end of file + + if not ( + hasattr(decorator_type, "from_dict") + and callable(getattr(decorator_type, "from_dict")) + ): + raise MissingMethodException( + "The type {decorator_type} does not have the required method called " + " 'from_dict'.".format(decorator_type=decorator_type) + ) diff --git a/tests/test_functions/typed_events/missing_from_dict.py b/tests/test_functions/typed_events/missing_from_dict.py index e20180ba..d1b6fa8e 100644 --- a/tests/test_functions/typed_events/missing_from_dict.py +++ b/tests/test_functions/typed_events/missing_from_dict.py @@ -13,20 +13,23 @@ # limitations under the License. """Function used to test handling functions using typed decorators.""" +from typing import Any, TypeVar import flask import functions_framework -from typing import Any, TypeVar - T = TypeVar("T") + def from_str(x: Any) -> str: assert isinstance(x, str) return x + def from_int(x: Any) -> int: assert isinstance(x, int) and not isinstance(x, bool) + return x + class TestTypeMissingFromDict: name: str @@ -42,12 +45,10 @@ def to_dict(self) -> dict: result["age"] = from_int(self.age) return result -@functions_framework.typednew(TestTypeMissingFromDict) -def function_typed_missing_from_dict(testType:TestTypeMissingFromDict): - valid_event =( - testType.name == "john" - and testType.age == 10 - ) + +@functions_framework.typed(TestTypeMissingFromDict) +def function_typed_missing_from_dict(test_type: TestTypeMissingFromDict): + valid_event = test_type.name == "john" and test_type.age == 10 if not valid_event: flask.abort(500) - return testType \ No newline at end of file + return test_type diff --git a/tests/test_functions/typed_events/missing_to_dict.py b/tests/test_functions/typed_events/missing_to_dict.py index ee6a7b63..eef0dc8e 100644 --- a/tests/test_functions/typed_events/missing_to_dict.py +++ b/tests/test_functions/typed_events/missing_to_dict.py @@ -21,13 +21,16 @@ T = TypeVar("T") + def from_str(x: Any) -> str: assert isinstance(x, str) return x + def from_int(x: Any) -> int: assert isinstance(x, int) and not isinstance(x, bool) - + return x + class TestTypeMissingToDict: name: str @@ -38,18 +41,15 @@ def __init__(self, name: str, age: int) -> None: self.age = age @staticmethod - def from_dict(obj: dict) -> 'TestTypeMissingToDict': + def from_dict(obj: dict) -> "TestTypeMissingToDict": name = from_str(obj.get("name")) age = from_int(obj.get("age")) return TestTypeMissingToDict(name, age) -@functions_framework.typednew(TestTypeMissingToDict) -def function_typed_missing_to_dict(testType:TestTypeMissingToDict): - valid_event =( - testType.name == "john" - and testType.age == 10 - ) +@functions_framework.typed(TestTypeMissingToDict) +def function_typed_missing_to_dict(testType: TestTypeMissingToDict): + valid_event = testType.name == "john" and testType.age == 10 if not valid_event: flask.abort(500) - return testType \ No newline at end of file + return testType diff --git a/tests/test_functions/typed_events/missing_type.py b/tests/test_functions/typed_events/missing_type.py index 2bca56eb..a06e699f 100644 --- a/tests/test_functions/typed_events/missing_type.py +++ b/tests/test_functions/typed_events/missing_type.py @@ -17,12 +17,10 @@ import functions_framework + @functions_framework.typednew def function_typed_missing_type_information(testType): - valid_event =( - testType.name == "john" - and testType.age == 10 - ) + valid_event = testType.name == "john" and testType.age == 10 if not valid_event: flask.abort(500) - return testType \ No newline at end of file + return testType diff --git a/tests/test_functions/typed_events/typed_event.py b/tests/test_functions/typed_events/typed_event.py index 13add756..af0cde73 100644 --- a/tests/test_functions/typed_events/typed_event.py +++ b/tests/test_functions/typed_events/typed_event.py @@ -21,18 +21,22 @@ T = TypeVar("T") + def from_str(x: Any) -> str: assert isinstance(x, str) return x + def from_int(x: Any) -> int: assert isinstance(x, int) and not isinstance(x, bool) return x + def to_class(c: Type[T], x: Any) -> dict: assert isinstance(x, c) return cast(Any, x).to_dict() + class TestType: name: str age: int @@ -42,7 +46,7 @@ def __init__(self, name: str, age: int) -> None: self.age = age @staticmethod - def from_dict(obj: dict) -> 'TestType': + def from_dict(obj: dict) -> "TestType": name = from_str(obj.get("name")) age = from_int(obj.get("age")) return TestType(name, age) @@ -54,15 +58,9 @@ def to_dict(self) -> dict: return result - - @functions_framework.typednew(TestType) -def function_typed(testType:TestType): - valid_event =( - testType.name == "john" - and testType.age == 10 - ) +def function_typed(testType: TestType): + valid_event = testType.name == "john" and testType.age == 10 if not valid_event: flask.abort(500) return testType - diff --git a/tests/test_typed_event_functions.py b/tests/test_typed_event_functions.py index 5ab1cdda..7c1387c6 100644 --- a/tests/test_typed_event_functions.py +++ b/tests/test_typed_event_functions.py @@ -34,16 +34,18 @@ def typed_decorator_client(): target = "function_typed" return create_app(target, source).test_client() + @pytest.fixture def typed_decorator_missing_todict(): source = TEST_FUNCTIONS_DIR / "typed_events" / "missing_to_dict.py" target = "function_typed_missing_to_dict" return create_app(target, source).test_client() + def test_typed_decorator(typed_decorator_client): - resp = typed_decorator_client.post("/", json={"name": "john" , "age": 10}) + resp = typed_decorator_client.post("/", json={"name": "john", "age": 10}) assert resp.status_code == 200 - assert resp.data == b"{\"age\":10,\"name\":\"john\"}\n" + assert resp.data == b'{"age":10,"name":"john"}\n' def test_missing_from_dict_typed_decorator(): @@ -59,6 +61,7 @@ def test_missing_type_information_typed_decorator(): with pytest.raises(MissingTypeException): create_app(target, source).test_client() + def test_missing_to_dict_typed_decorator(typed_decorator_missing_todict): - resp = typed_decorator_missing_todict.post("/", json={"name": "john" , "age": 10}) + resp = typed_decorator_missing_todict.post("/", json={"name": "john", "age": 10}) assert resp.status_code == 500 From 7febdf2a0cc65914091006a41ab96d846ac32aa2 Mon Sep 17 00:00:00 2001 From: Pratiksha Kap Date: Tue, 29 Nov 2022 13:15:02 -0800 Subject: [PATCH 10/16] fix imports --- src/functions_framework/__init__.py | 3 ++- src/functions_framework/_function_registry.py | 3 ++- src/functions_framework/typed_event.py | 5 +++-- tests/test_functions/typed_events/missing_from_dict.py | 1 + tests/test_functions/typed_events/missing_to_dict.py | 4 ++-- tests/test_functions/typed_events/typed_event.py | 4 ++-- tests/test_typed_event_functions.py | 1 - 7 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index c70e1aa6..248efa4d 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -13,7 +13,6 @@ # limitations under the License. import functools -from inspect import signature import inspect import io import json @@ -22,6 +21,8 @@ import pathlib import sys import types + +from inspect import signature from typing import Type import cloudevents.exceptions as cloud_exceptions diff --git a/src/functions_framework/_function_registry.py b/src/functions_framework/_function_registry.py index 102410b9..9722af28 100644 --- a/src/functions_framework/_function_registry.py +++ b/src/functions_framework/_function_registry.py @@ -13,9 +13,10 @@ # limitations under the License. import importlib.util import os -from re import T import sys import types + +from re import T from typing import Type from functions_framework.exceptions import ( diff --git a/src/functions_framework/typed_event.py b/src/functions_framework/typed_event.py index a7450c54..ff910165 100644 --- a/src/functions_framework/typed_event.py +++ b/src/functions_framework/typed_event.py @@ -13,15 +13,16 @@ # limitations under the License. -from inspect import signature import inspect +from inspect import signature + +from functions_framework import _function_registry from functions_framework.exceptions import ( MissingMethodException, MissingTypeException, TypeMismatchException, ) -from functions_framework import _function_registry class TypedEvent(object): diff --git a/tests/test_functions/typed_events/missing_from_dict.py b/tests/test_functions/typed_events/missing_from_dict.py index d1b6fa8e..91f4ae28 100644 --- a/tests/test_functions/typed_events/missing_from_dict.py +++ b/tests/test_functions/typed_events/missing_from_dict.py @@ -14,6 +14,7 @@ """Function used to test handling functions using typed decorators.""" from typing import Any, TypeVar + import flask import functions_framework diff --git a/tests/test_functions/typed_events/missing_to_dict.py b/tests/test_functions/typed_events/missing_to_dict.py index eef0dc8e..ac1d0f15 100644 --- a/tests/test_functions/typed_events/missing_to_dict.py +++ b/tests/test_functions/typed_events/missing_to_dict.py @@ -13,12 +13,12 @@ # limitations under the License. """Function used to test handling functions using typed decorators.""" +from typing import Any, TypeVar + import flask import functions_framework -from typing import Any, TypeVar - T = TypeVar("T") diff --git a/tests/test_functions/typed_events/typed_event.py b/tests/test_functions/typed_events/typed_event.py index af0cde73..ec07d74a 100644 --- a/tests/test_functions/typed_events/typed_event.py +++ b/tests/test_functions/typed_events/typed_event.py @@ -13,12 +13,12 @@ # limitations under the License. """Function used to test handling functions using typed decorators.""" +from typing import Any, Type, TypeVar, cast + import flask import functions_framework -from typing import Any, TypeVar, Type, cast - T = TypeVar("T") diff --git a/tests/test_typed_event_functions.py b/tests/test_typed_event_functions.py index 7c1387c6..74a7857f 100644 --- a/tests/test_typed_event_functions.py +++ b/tests/test_typed_event_functions.py @@ -15,7 +15,6 @@ import pytest - from functions_framework import create_app from functions_framework.exceptions import MissingMethodException, MissingTypeException From 4526f7a8091838a4a7c3546f3c2f81040a414c49 Mon Sep 17 00:00:00 2001 From: Pratiksha Kap Date: Fri, 2 Dec 2022 11:07:49 -0800 Subject: [PATCH 11/16] use built-in exceptions --- src/functions_framework/__init__.py | 2 +- src/functions_framework/_function_registry.py | 1 + src/functions_framework/exceptions.py | 12 ------- src/functions_framework/typed_event.py | 31 +++++++++++++------ .../typed_events/missing_type.py | 2 +- .../typed_events/typed_event.py | 2 +- tests/test_typed_event_functions.py | 7 ++--- 7 files changed, 29 insertions(+), 28 deletions(-) diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index 248efa4d..a8257591 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -147,7 +147,7 @@ def view_func(path): if response.__class__.__module__ == "builtins": return response typed_event.validate_return_type(response) - return response.to_dict() + return json.dumps(response.to_dict()) return view_func diff --git a/src/functions_framework/_function_registry.py b/src/functions_framework/_function_registry.py index 9722af28..6f1c5498 100644 --- a/src/functions_framework/_function_registry.py +++ b/src/functions_framework/_function_registry.py @@ -37,6 +37,7 @@ # Keys are user function names, values are user function signature types. REGISTRY_MAP = {} INPUT_MAP = {} +CONTEXT_MAP = {} def get_user_function(source, source_module, target): diff --git a/src/functions_framework/exceptions.py b/src/functions_framework/exceptions.py index bc6da58f..671a28a4 100644 --- a/src/functions_framework/exceptions.py +++ b/src/functions_framework/exceptions.py @@ -35,15 +35,3 @@ class MissingTargetException(FunctionsFrameworkException): class EventConversionException(FunctionsFrameworkException): pass - - -class MissingTypeException(FunctionsFrameworkException): - pass - - -class TypeMismatchException(FunctionsFrameworkException): - pass - - -class MissingMethodException(FunctionsFrameworkException): - pass diff --git a/src/functions_framework/typed_event.py b/src/functions_framework/typed_event.py index ff910165..5233e8df 100644 --- a/src/functions_framework/typed_event.py +++ b/src/functions_framework/typed_event.py @@ -18,11 +18,6 @@ from inspect import signature from functions_framework import _function_registry -from functions_framework.exceptions import ( - MissingMethodException, - MissingTypeException, - TypeMismatchException, -) class TypedEvent(object): @@ -35,6 +30,24 @@ def __init__( self.data = data +def register_typed_event2(decorator_type, contextSet, func): + print("######") + print(decorator_type) + print(contextSet) + sig = signature(func) + annotation_type = list(sig.parameters.values())[0].annotation + print(annotation_type) + type_validity_check(decorator_type, annotation_type) + if decorator_type == "": + decorator_type = annotation_type + + _function_registry.INPUT_MAP[func.__name__] = decorator_type + _function_registry.REGISTRY_MAP[ + func.__name__ + ] = _function_registry.TYPED_SIGNATURE_TYPE + _function_registry.CONTEXT_MAP[func.__name__] = contextSet + + def register_typed_event(decorator_type, func): sig = signature(func) annotation_type = list(sig.parameters.values())[0].annotation @@ -51,7 +64,7 @@ def register_typed_event(decorator_type, func): def validate_return_type(response): if not (hasattr(response, "to_dict") and callable(getattr(response, "to_dict"))): - raise MissingMethodException( + raise AttributeError( "The type {response} does not have the required method called " " 'to_dict'.".format(response=response) ) @@ -59,7 +72,7 @@ def validate_return_type(response): def type_validity_check(decorator_type, annotation_type): if decorator_type == "" and annotation_type is inspect._empty: - raise MissingTypeException( + raise TypeError( "The function defined does not contain Type of the input object." ) @@ -68,7 +81,7 @@ def type_validity_check(decorator_type, annotation_type): and annotation_type is not inspect._empty and decorator_type != annotation_type ): - raise TypeMismatchException( + raise TypeError( "The object type provided via 'typed' {decorator_type}" "is different from the one in the function annotation {annotation_type}.".format( decorator_type=decorator_type, annotation_type=annotation_type @@ -82,7 +95,7 @@ def type_validity_check(decorator_type, annotation_type): hasattr(decorator_type, "from_dict") and callable(getattr(decorator_type, "from_dict")) ): - raise MissingMethodException( + raise AttributeError( "The type {decorator_type} does not have the required method called " " 'from_dict'.".format(decorator_type=decorator_type) ) diff --git a/tests/test_functions/typed_events/missing_type.py b/tests/test_functions/typed_events/missing_type.py index a06e699f..aab93a8c 100644 --- a/tests/test_functions/typed_events/missing_type.py +++ b/tests/test_functions/typed_events/missing_type.py @@ -18,7 +18,7 @@ import functions_framework -@functions_framework.typednew +@functions_framework.typed def function_typed_missing_type_information(testType): valid_event = testType.name == "john" and testType.age == 10 if not valid_event: diff --git a/tests/test_functions/typed_events/typed_event.py b/tests/test_functions/typed_events/typed_event.py index ec07d74a..2b5a470f 100644 --- a/tests/test_functions/typed_events/typed_event.py +++ b/tests/test_functions/typed_events/typed_event.py @@ -58,7 +58,7 @@ def to_dict(self) -> dict: return result -@functions_framework.typednew(TestType) +@functions_framework.typed(TestType) def function_typed(testType: TestType): valid_event = testType.name == "john" and testType.age == 10 if not valid_event: diff --git a/tests/test_typed_event_functions.py b/tests/test_typed_event_functions.py index 74a7857f..7f9a3492 100644 --- a/tests/test_typed_event_functions.py +++ b/tests/test_typed_event_functions.py @@ -16,7 +16,6 @@ import pytest from functions_framework import create_app -from functions_framework.exceptions import MissingMethodException, MissingTypeException TEST_FUNCTIONS_DIR = pathlib.Path(__file__).resolve().parent / "test_functions" @@ -44,20 +43,20 @@ def typed_decorator_missing_todict(): def test_typed_decorator(typed_decorator_client): resp = typed_decorator_client.post("/", json={"name": "john", "age": 10}) assert resp.status_code == 200 - assert resp.data == b'{"age":10,"name":"john"}\n' + assert resp.data == b'{"name": "john", "age": 10}' def test_missing_from_dict_typed_decorator(): source = TEST_FUNCTIONS_DIR / "typed_events" / "missing_from_dict.py" target = "function_typed_missing_from_dict" - with pytest.raises(MissingMethodException) as excinfo: + with pytest.raises(AttributeError) as excinfo: create_app(target, source).test_client() def test_missing_type_information_typed_decorator(): source = TEST_FUNCTIONS_DIR / "typed_events" / "missing_type.py" target = "function_typed_missing_type_information" - with pytest.raises(MissingTypeException): + with pytest.raises(TypeError): create_app(target, source).test_client() From 2593071a56cc44d0bb56be55de0a61e429c49245 Mon Sep 17 00:00:00 2001 From: Pratiksha Kap Date: Mon, 5 Dec 2022 00:01:10 -0800 Subject: [PATCH 12/16] add more tests --- src/functions_framework/typed_event.py | 28 ------------ .../typed_events/mismatch_types.py | 43 +++++++++++++++++++ .../typed_events/typed_event.py | 23 ++++++++++ tests/test_typed_event_functions.py | 33 +++++++++++++- 4 files changed, 97 insertions(+), 30 deletions(-) create mode 100644 tests/test_functions/typed_events/mismatch_types.py diff --git a/src/functions_framework/typed_event.py b/src/functions_framework/typed_event.py index 5233e8df..fca9f31a 100644 --- a/src/functions_framework/typed_event.py +++ b/src/functions_framework/typed_event.py @@ -20,34 +20,6 @@ from functions_framework import _function_registry -class TypedEvent(object): - - # Supports v1beta1, v1beta2, and v1 event formats. - def __init__( - self, - data, - ): - self.data = data - - -def register_typed_event2(decorator_type, contextSet, func): - print("######") - print(decorator_type) - print(contextSet) - sig = signature(func) - annotation_type = list(sig.parameters.values())[0].annotation - print(annotation_type) - type_validity_check(decorator_type, annotation_type) - if decorator_type == "": - decorator_type = annotation_type - - _function_registry.INPUT_MAP[func.__name__] = decorator_type - _function_registry.REGISTRY_MAP[ - func.__name__ - ] = _function_registry.TYPED_SIGNATURE_TYPE - _function_registry.CONTEXT_MAP[func.__name__] = contextSet - - def register_typed_event(decorator_type, func): sig = signature(func) annotation_type = list(sig.parameters.values())[0].annotation diff --git a/tests/test_functions/typed_events/mismatch_types.py b/tests/test_functions/typed_events/mismatch_types.py new file mode 100644 index 00000000..8fe37031 --- /dev/null +++ b/tests/test_functions/typed_events/mismatch_types.py @@ -0,0 +1,43 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Function used to test handling functions using typed decorators.""" + +import flask + +import functions_framework + + +class TestType1: + name: str + age: int + + def __init__(self, name: str, age: int) -> None: + self.name = name + self.age = age + + +class TestType2: + name: str + + def __init__(self, name: str) -> None: + self.name = name + + +@functions_framework.typed(TestType2) +def function_typed_mismatch_types(test_type: TestType1): + valid_event = test_type.name == "john" and test_type.age == 10 + if not valid_event: + flask.abort(500) + return test_type diff --git a/tests/test_functions/typed_events/typed_event.py b/tests/test_functions/typed_events/typed_event.py index 2b5a470f..d5ea093b 100644 --- a/tests/test_functions/typed_events/typed_event.py +++ b/tests/test_functions/typed_events/typed_event.py @@ -64,3 +64,26 @@ def function_typed(testType: TestType): if not valid_event: flask.abort(500) return testType + + +@functions_framework.typed +def function_typed_reflect(testType: TestType): + valid_event = testType.name == "jane" and testType.age == 20 + if not valid_event: + flask.abort(500) + return testType + + +@functions_framework.typed +def function_typed_noreturn(testType: TestType): + valid_event = testType.name == "jane" and testType.age == 20 + if not valid_event: + flask.abort(500) + + +@functions_framework.typed +def function_typed_string_return(testType: TestType): + valid_event = testType.name == "jane" and testType.age == 20 + if not valid_event: + flask.abort(500) + return "Hello " + testType.name diff --git a/tests/test_typed_event_functions.py b/tests/test_typed_event_functions.py index 7f9a3492..17aa1d11 100644 --- a/tests/test_typed_event_functions.py +++ b/tests/test_typed_event_functions.py @@ -27,9 +27,9 @@ @pytest.fixture -def typed_decorator_client(): +def typed_decorator_client(function_name): source = TEST_FUNCTIONS_DIR / "typed_events" / "typed_event.py" - target = "function_typed" + target = function_name return create_app(target, source).test_client() @@ -40,12 +40,34 @@ def typed_decorator_missing_todict(): return create_app(target, source).test_client() +@pytest.mark.parametrize("function_name", ["function_typed"]) def test_typed_decorator(typed_decorator_client): resp = typed_decorator_client.post("/", json={"name": "john", "age": 10}) assert resp.status_code == 200 assert resp.data == b'{"name": "john", "age": 10}' +@pytest.mark.parametrize("function_name", ["function_typed_reflect"]) +def test_typed_reflect_decorator(typed_decorator_client): + resp = typed_decorator_client.post("/", json={"name": "jane", "age": 20}) + assert resp.status_code == 200 + assert resp.data == b'{"name": "jane", "age": 20}' + + +@pytest.mark.parametrize("function_name", ["function_typed_noreturn"]) +def test_typed_noreturn(typed_decorator_client): + resp = typed_decorator_client.post("/", json={"name": "jane", "age": 20}) + assert resp.status_code == 200 + assert resp.data == b"OK" + + +@pytest.mark.parametrize("function_name", ["function_typed_string_return"]) +def test_typed_string_return(typed_decorator_client): + resp = typed_decorator_client.post("/", json={"name": "jane", "age": 20}) + assert resp.status_code == 200 + assert resp.data == b"Hello jane" + + def test_missing_from_dict_typed_decorator(): source = TEST_FUNCTIONS_DIR / "typed_events" / "missing_from_dict.py" target = "function_typed_missing_from_dict" @@ -53,6 +75,13 @@ def test_missing_from_dict_typed_decorator(): create_app(target, source).test_client() +def test_mismatch_types_typed_decorator(): + source = TEST_FUNCTIONS_DIR / "typed_events" / "mismatch_types.py" + target = "function_typed_mismatch_types" + with pytest.raises(TypeError) as excinfo: + create_app(target, source).test_client() + + def test_missing_type_information_typed_decorator(): source = TEST_FUNCTIONS_DIR / "typed_events" / "missing_type.py" target = "function_typed_missing_type_information" From 66e5569a7d022d493b4dff5f5a6802c02ad6b7c2 Mon Sep 17 00:00:00 2001 From: Pratiksha Kap Date: Wed, 7 Dec 2022 05:31:39 -0800 Subject: [PATCH 13/16] address comments --- src/functions_framework/__init__.py | 37 ++++--- src/functions_framework/_function_registry.py | 8 +- src/functions_framework/_typed_event.py | 99 +++++++++++++++++++ src/functions_framework/event_conversion.py | 4 - src/functions_framework/typed_event.py | 73 -------------- .../typed_events/mismatch_types.py | 2 +- .../typed_events/missing_from_dict.py | 2 +- .../typed_events/missing_parameter.py | 23 +++++ .../typed_events/missing_to_dict.py | 2 +- .../typed_events/missing_type.py | 2 +- .../typed_events/typed_event.py | 2 +- tests/test_typed_event_functions.py | 16 ++- 12 files changed, 164 insertions(+), 106 deletions(-) create mode 100644 src/functions_framework/_typed_event.py delete mode 100644 src/functions_framework/typed_event.py create mode 100644 tests/test_functions/typed_events/missing_parameter.py diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index a8257591..c8a9f56d 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -31,7 +31,7 @@ from cloudevents.http import from_http, is_binary -from functions_framework import _function_registry, event_conversion, typed_event +from functions_framework import _function_registry, _typed_event, event_conversion from functions_framework.background_event import BackgroundEvent from functions_framework.exceptions import ( EventConversionException, @@ -76,7 +76,7 @@ def typed(googleType): # no parameter to the decorator if isinstance(googleType, types.FunctionType): func = googleType - typed_event.register_typed_event("", func) + _typed_event.register_typed_event("", func) @functools.wraps(func) def wrapper(*args, **kwargs): @@ -87,7 +87,7 @@ def wrapper(*args, **kwargs): else: def func_decorator(func): - typed_event.register_typed_event(googleType, func) + _typed_event.register_typed_event(googleType, func) @functools.wraps(func) def wrapper(*args, **kwargs): @@ -139,15 +139,20 @@ def _run_cloud_event(function, request): def _typed_event_func_wrapper(function, request, inputType: Type): def view_func(path): - data = request.get_json() - input = inputType.from_dict(data) - response = function(input) - if response is None: - return "OK" - if response.__class__.__module__ == "builtins": - return response - typed_event.validate_return_type(response) - return json.dumps(response.to_dict()) + try: + data = request.get_json() + input = inputType.from_dict(data) + response = function(input) + if response is None: + return "", 200 + if response.__class__.__module__ == "builtins": + return response + _typed_event.validate_return_type(response) + return json.dumps(response.to_dict()) + except Exception as e: + raise FunctionsFrameworkException( + "Function execution failed with the error" + ) from e return view_func @@ -220,7 +225,7 @@ def view_func(path): return view_func -def _configure_app(app, function, signature_type, inputType): +def _configure_app(app, function, signature_type): # Mount the function at the root. Support GCF's default path behavior # Modify the url_map and view_functions directly here instead of using # add_url_rule in order to create endpoints that route all methods @@ -274,8 +279,9 @@ def _configure_app(app, function, signature_type, inputType): "/", endpoint=signature_type, methods=["POST"] ) ) + input_type = _function_registry.get_func_input_type(function.__name__) app.view_functions[signature_type] = _typed_event_func_wrapper( - function, flask.request, inputType + function, flask.request, input_type ) else: raise FunctionsFrameworkException( @@ -349,9 +355,8 @@ def handle_none(rv): # Get the configured function signature type signature_type = _function_registry.get_func_signature_type(target, signature_type) function = _function_registry.get_user_function(source, source_module, target) - inputType = _function_registry.get_func_input_type(target) - _configure_app(_app, function, signature_type, inputType) + _configure_app(_app, function, signature_type) return _app diff --git a/src/functions_framework/_function_registry.py b/src/functions_framework/_function_registry.py index 6f1c5498..f266ee82 100644 --- a/src/functions_framework/_function_registry.py +++ b/src/functions_framework/_function_registry.py @@ -36,8 +36,10 @@ # REGISTRY_MAP stores the registered functions. # Keys are user function names, values are user function signature types. REGISTRY_MAP = {} -INPUT_MAP = {} -CONTEXT_MAP = {} + +# INPUT_TYPE_MAP stores the input type of the typed functions. +# Keys are the user function name, values are the type of the function input +INPUT_TYPE_MAP = {} def get_user_function(source, source_module, target): @@ -129,5 +131,5 @@ def get_func_signature_type(func_name: str, signature_type: str) -> str: def get_func_input_type(func_name: str) -> Type: - registered_type = INPUT_MAP[func_name] if func_name in INPUT_MAP else "" + registered_type = INPUT_TYPE_MAP[func_name] if func_name in INPUT_TYPE_MAP else "" return registered_type diff --git a/src/functions_framework/_typed_event.py b/src/functions_framework/_typed_event.py new file mode 100644 index 00000000..3e1d8cee --- /dev/null +++ b/src/functions_framework/_typed_event.py @@ -0,0 +1,99 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import inspect + +from inspect import signature + +from functions_framework import _function_registry +from functions_framework.exceptions import FunctionsFrameworkException + +"""Registers user function in the REGISTRY_MAP and the INPUT_TYPE_MAP. +Also performs some validity checks for the input type of the function + +Args: + decorator_type: The type provided by the @typed(input_type) decorator + func: User function +""" + + +def register_typed_event(decorator_type, func): + try: + sig = signature(func) + annotation_type = list(sig.parameters.values())[0].annotation + input_type = _select_input_type(decorator_type, annotation_type) + _type_validity_check(input_type) + except IndexError: + raise FunctionsFrameworkException( + "Function signature is missing an input parameter." + "The function should be defined as def your_fn(in: input_type)" + ) + except Exception as e: + raise FunctionsFrameworkException( + "Functions using the @typed decorator must provide " + "the type of the input parameter by specifying @typed(input_type) and/or using python " + "type annotations def your_fn(in: input_type)." + ) + + _function_registry.INPUT_TYPE_MAP[func.__name__] = input_type + _function_registry.REGISTRY_MAP[ + func.__name__ + ] = _function_registry.TYPED_SIGNATURE_TYPE + + +""" Checks whether the response type of the typed function has a to_dict method""" +def validate_return_type(response): + if not (hasattr(response, "to_dict") and callable(getattr(response, "to_dict"))): + raise AttributeError( + "The type {response} does not have the required method called " + " 'to_dict'.".format(response=type(response)) + ) + + +"""Selects the input type for the typed function provided through the @typed(input_type) +decorator or through the parameter annotation in the user function +""" +def _select_input_type(decorator_type, annotation_type): + if decorator_type == "" and annotation_type is inspect._empty: + raise TypeError( + "The function defined does not contain Type of the input object." + ) + + if ( + decorator_type != "" + and annotation_type is not inspect._empty + and decorator_type != annotation_type + ): + raise TypeError( + "The object type provided via 'typed' {decorator_type}" + "is different from the one in the function annotation {annotation_type}.".format( + decorator_type=decorator_type, annotation_type=annotation_type + ) + ) + + if decorator_type == "": + return annotation_type + return decorator_type + + +"""Checks for the from_dict method implementation in the input type class""" +def _type_validity_check(input_type): + if not ( + hasattr(input_type, "from_dict") and callable(getattr(input_type, "from_dict")) + ): + raise AttributeError( + "The type {decorator_type} does not have the required method called " + " 'from_dict'.".format(decorator_type=input_type) + ) diff --git a/src/functions_framework/event_conversion.py b/src/functions_framework/event_conversion.py index 458bcd7d..06e5a812 100644 --- a/src/functions_framework/event_conversion.py +++ b/src/functions_framework/event_conversion.py @@ -115,7 +115,6 @@ def background_event_to_cloud_event(request) -> CloudEvent: - print("background_event_to_cloud_event") """Converts a background event represented by the given HTTP request into a CloudEvent.""" event_data = marshal_background_event_data(request) if not event_data: @@ -186,7 +185,6 @@ def background_event_to_cloud_event(request) -> CloudEvent: def is_convertable_cloud_event(request) -> bool: - print("is_convertable_cloud_event") """Is the given request a known CloudEvent that can be converted to background event.""" if is_binary(request.headers): event_type = request.headers.get("ce-type") @@ -210,7 +208,6 @@ def _split_ce_source(source) -> Tuple[str, str]: def cloud_event_to_background_event(request) -> Tuple[Any, Context]: - print("cloud_event_to_background_event") """Converts a background event represented by the given HTTP request into a CloudEvent.""" try: event = from_http(request.headers, request.get_data()) @@ -297,7 +294,6 @@ def _split_resource(context: Context) -> Tuple[str, str, str]: def marshal_background_event_data(request): - print("marshal_background_event_data") """Marshal the request body of a raw Pub/Sub HTTP request into the schema that is expected of a background event""" try: diff --git a/src/functions_framework/typed_event.py b/src/functions_framework/typed_event.py deleted file mode 100644 index fca9f31a..00000000 --- a/src/functions_framework/typed_event.py +++ /dev/null @@ -1,73 +0,0 @@ -# Copyright 2021 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import inspect - -from inspect import signature - -from functions_framework import _function_registry - - -def register_typed_event(decorator_type, func): - sig = signature(func) - annotation_type = list(sig.parameters.values())[0].annotation - - type_validity_check(decorator_type, annotation_type) - if decorator_type == "": - decorator_type = annotation_type - - _function_registry.INPUT_MAP[func.__name__] = decorator_type - _function_registry.REGISTRY_MAP[ - func.__name__ - ] = _function_registry.TYPED_SIGNATURE_TYPE - - -def validate_return_type(response): - if not (hasattr(response, "to_dict") and callable(getattr(response, "to_dict"))): - raise AttributeError( - "The type {response} does not have the required method called " - " 'to_dict'.".format(response=response) - ) - - -def type_validity_check(decorator_type, annotation_type): - if decorator_type == "" and annotation_type is inspect._empty: - raise TypeError( - "The function defined does not contain Type of the input object." - ) - - if ( - decorator_type != "" - and annotation_type is not inspect._empty - and decorator_type != annotation_type - ): - raise TypeError( - "The object type provided via 'typed' {decorator_type}" - "is different from the one in the function annotation {annotation_type}.".format( - decorator_type=decorator_type, annotation_type=annotation_type - ) - ) - - if decorator_type == "": - decorator_type = annotation_type - - if not ( - hasattr(decorator_type, "from_dict") - and callable(getattr(decorator_type, "from_dict")) - ): - raise AttributeError( - "The type {decorator_type} does not have the required method called " - " 'from_dict'.".format(decorator_type=decorator_type) - ) diff --git a/tests/test_functions/typed_events/mismatch_types.py b/tests/test_functions/typed_events/mismatch_types.py index 8fe37031..4196b23f 100644 --- a/tests/test_functions/typed_events/mismatch_types.py +++ b/tests/test_functions/typed_events/mismatch_types.py @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/test_functions/typed_events/missing_from_dict.py b/tests/test_functions/typed_events/missing_from_dict.py index 91f4ae28..9aee0b32 100644 --- a/tests/test_functions/typed_events/missing_from_dict.py +++ b/tests/test_functions/typed_events/missing_from_dict.py @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/test_functions/typed_events/missing_parameter.py b/tests/test_functions/typed_events/missing_parameter.py new file mode 100644 index 00000000..64681d8e --- /dev/null +++ b/tests/test_functions/typed_events/missing_parameter.py @@ -0,0 +1,23 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Function used to test handling functions using typed decorators.""" +import flask + +import functions_framework + + +@functions_framework.typed +def function_typed_missing_type_information(): + print("hello") diff --git a/tests/test_functions/typed_events/missing_to_dict.py b/tests/test_functions/typed_events/missing_to_dict.py index ac1d0f15..9b9dd43b 100644 --- a/tests/test_functions/typed_events/missing_to_dict.py +++ b/tests/test_functions/typed_events/missing_to_dict.py @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/test_functions/typed_events/missing_type.py b/tests/test_functions/typed_events/missing_type.py index aab93a8c..9df14c44 100644 --- a/tests/test_functions/typed_events/missing_type.py +++ b/tests/test_functions/typed_events/missing_type.py @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/test_functions/typed_events/typed_event.py b/tests/test_functions/typed_events/typed_event.py index d5ea093b..83a8bd82 100644 --- a/tests/test_functions/typed_events/typed_event.py +++ b/tests/test_functions/typed_events/typed_event.py @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/test_typed_event_functions.py b/tests/test_typed_event_functions.py index 17aa1d11..227bb10a 100644 --- a/tests/test_typed_event_functions.py +++ b/tests/test_typed_event_functions.py @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ import pytest from functions_framework import create_app +from functions_framework.exceptions import FunctionsFrameworkException TEST_FUNCTIONS_DIR = pathlib.Path(__file__).resolve().parent / "test_functions" @@ -58,7 +59,7 @@ def test_typed_reflect_decorator(typed_decorator_client): def test_typed_noreturn(typed_decorator_client): resp = typed_decorator_client.post("/", json={"name": "jane", "age": 20}) assert resp.status_code == 200 - assert resp.data == b"OK" + assert resp.data == b"" @pytest.mark.parametrize("function_name", ["function_typed_string_return"]) @@ -71,23 +72,28 @@ def test_typed_string_return(typed_decorator_client): def test_missing_from_dict_typed_decorator(): source = TEST_FUNCTIONS_DIR / "typed_events" / "missing_from_dict.py" target = "function_typed_missing_from_dict" - with pytest.raises(AttributeError) as excinfo: + with pytest.raises(FunctionsFrameworkException) as excinfo: create_app(target, source).test_client() def test_mismatch_types_typed_decorator(): source = TEST_FUNCTIONS_DIR / "typed_events" / "mismatch_types.py" target = "function_typed_mismatch_types" - with pytest.raises(TypeError) as excinfo: + with pytest.raises(FunctionsFrameworkException) as excinfo: create_app(target, source).test_client() def test_missing_type_information_typed_decorator(): source = TEST_FUNCTIONS_DIR / "typed_events" / "missing_type.py" target = "function_typed_missing_type_information" - with pytest.raises(TypeError): + with pytest.raises(FunctionsFrameworkException): create_app(target, source).test_client() +def test_missing_parameter_typed_decorator(): + source = TEST_FUNCTIONS_DIR / "typed_events" / "missing_parameter.py" + target = "function_typed_missing_parameter" + with pytest.raises(FunctionsFrameworkException): + create_app(target, source).test_client() def test_missing_to_dict_typed_decorator(typed_decorator_missing_todict): resp = typed_decorator_missing_todict.post("/", json={"name": "john", "age": 10}) From fc2e7c5b03c2f918c41c82d52093b44dec138e34 Mon Sep 17 00:00:00 2001 From: Pratiksha Kap Date: Thu, 8 Dec 2022 10:59:53 -0800 Subject: [PATCH 14/16] refactor the decorator function --- src/functions_framework/__init__.py | 29 +++++++------------ src/functions_framework/_typed_event.py | 22 +++++++++----- .../typed_events/mismatch_types.py | 2 +- .../typed_events/missing_from_dict.py | 2 +- .../typed_events/missing_to_dict.py | 2 +- .../typed_events/missing_type.py | 2 +- .../typed_events/typed_event.py | 10 +++---- tests/test_typed_event_functions.py | 6 ++-- 8 files changed, 37 insertions(+), 38 deletions(-) diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index c8a9f56d..2cb53797 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -72,30 +72,22 @@ def wrapper(*args, **kwargs): return wrapper -def typed(googleType): - # no parameter to the decorator - if isinstance(googleType, types.FunctionType): - func = googleType - _typed_event.register_typed_event("", func) +def typed(*args): + def _typed(func): + _typed_event.register_typed_event(google_type, func) @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper - # type parameter provided to the decorator - else: - - def func_decorator(func): - _typed_event.register_typed_event(googleType, func) - - @functools.wraps(func) - def wrapper(*args, **kwargs): - return func(*args, **kwargs) - return wrapper - - return func_decorator + if len(args) == 1 and isinstance(args[0], types.FunctionType): + google_type = "" + return _typed(args[0]) + else: + google_type = args[0] + return _typed def http(func): @@ -147,7 +139,7 @@ def view_func(path): return "", 200 if response.__class__.__module__ == "builtins": return response - _typed_event.validate_return_type(response) + _typed_event._validate_return_type(response) return json.dumps(response.to_dict()) except Exception as e: raise FunctionsFrameworkException( @@ -268,7 +260,6 @@ def _configure_app(app, function, signature_type): function, flask.request ) elif signature_type == _function_registry.TYPED_SIGNATURE_TYPE: - # validity_check() app.url_map.add( werkzeug.routing.Rule( "/", defaults={"path": ""}, endpoint=signature_type, methods=["POST"] diff --git a/src/functions_framework/_typed_event.py b/src/functions_framework/_typed_event.py index 3e1d8cee..2f638665 100644 --- a/src/functions_framework/_typed_event.py +++ b/src/functions_framework/_typed_event.py @@ -34,17 +34,17 @@ def register_typed_event(decorator_type, func): sig = signature(func) annotation_type = list(sig.parameters.values())[0].annotation input_type = _select_input_type(decorator_type, annotation_type) - _type_validity_check(input_type) + _validate_input_type(input_type) except IndexError: raise FunctionsFrameworkException( "Function signature is missing an input parameter." - "The function should be defined as def your_fn(in: input_type)" + "The function should be defined as 'def your_fn(in: inputType)'" ) except Exception as e: raise FunctionsFrameworkException( "Functions using the @typed decorator must provide " - "the type of the input parameter by specifying @typed(input_type) and/or using python " - "type annotations def your_fn(in: input_type)." + "the type of the input parameter by specifying @typed(inputType) and/or using python " + "type annotations 'def your_fn(in: inputType)'" ) _function_registry.INPUT_TYPE_MAP[func.__name__] = input_type @@ -54,7 +54,9 @@ def register_typed_event(decorator_type, func): """ Checks whether the response type of the typed function has a to_dict method""" -def validate_return_type(response): + + +def _validate_return_type(response): if not (hasattr(response, "to_dict") and callable(getattr(response, "to_dict"))): raise AttributeError( "The type {response} does not have the required method called " @@ -65,6 +67,8 @@ def validate_return_type(response): """Selects the input type for the typed function provided through the @typed(input_type) decorator or through the parameter annotation in the user function """ + + def _select_input_type(decorator_type, annotation_type): if decorator_type == "" and annotation_type is inspect._empty: raise TypeError( @@ -77,8 +81,8 @@ def _select_input_type(decorator_type, annotation_type): and decorator_type != annotation_type ): raise TypeError( - "The object type provided via 'typed' {decorator_type}" - "is different from the one in the function annotation {annotation_type}.".format( + "The object type provided via 'typed' decorator: '{decorator_type}'" + "is different than the one specified by the function parameter's type annotation : '{annotation_type}'.".format( decorator_type=decorator_type, annotation_type=annotation_type ) ) @@ -89,7 +93,9 @@ def _select_input_type(decorator_type, annotation_type): """Checks for the from_dict method implementation in the input type class""" -def _type_validity_check(input_type): + + +def _validate_input_type(input_type): if not ( hasattr(input_type, "from_dict") and callable(getattr(input_type, "from_dict")) ): diff --git a/tests/test_functions/typed_events/mismatch_types.py b/tests/test_functions/typed_events/mismatch_types.py index 4196b23f..0f238d9c 100644 --- a/tests/test_functions/typed_events/mismatch_types.py +++ b/tests/test_functions/typed_events/mismatch_types.py @@ -39,5 +39,5 @@ def __init__(self, name: str) -> None: def function_typed_mismatch_types(test_type: TestType1): valid_event = test_type.name == "john" and test_type.age == 10 if not valid_event: - flask.abort(500) + raise Exception("Received invalid input") return test_type diff --git a/tests/test_functions/typed_events/missing_from_dict.py b/tests/test_functions/typed_events/missing_from_dict.py index 9aee0b32..73a2cf93 100644 --- a/tests/test_functions/typed_events/missing_from_dict.py +++ b/tests/test_functions/typed_events/missing_from_dict.py @@ -51,5 +51,5 @@ def to_dict(self) -> dict: def function_typed_missing_from_dict(test_type: TestTypeMissingFromDict): valid_event = test_type.name == "john" and test_type.age == 10 if not valid_event: - flask.abort(500) + raise Exception("Received invalid input") return test_type diff --git a/tests/test_functions/typed_events/missing_to_dict.py b/tests/test_functions/typed_events/missing_to_dict.py index 9b9dd43b..76c95344 100644 --- a/tests/test_functions/typed_events/missing_to_dict.py +++ b/tests/test_functions/typed_events/missing_to_dict.py @@ -51,5 +51,5 @@ def from_dict(obj: dict) -> "TestTypeMissingToDict": def function_typed_missing_to_dict(testType: TestTypeMissingToDict): valid_event = testType.name == "john" and testType.age == 10 if not valid_event: - flask.abort(500) + raise Exception("Received invalid input") return testType diff --git a/tests/test_functions/typed_events/missing_type.py b/tests/test_functions/typed_events/missing_type.py index 9df14c44..1f35c0d6 100644 --- a/tests/test_functions/typed_events/missing_type.py +++ b/tests/test_functions/typed_events/missing_type.py @@ -22,5 +22,5 @@ def function_typed_missing_type_information(testType): valid_event = testType.name == "john" and testType.age == 10 if not valid_event: - flask.abort(500) + raise Exception("Received invalid input") return testType diff --git a/tests/test_functions/typed_events/typed_event.py b/tests/test_functions/typed_events/typed_event.py index 83a8bd82..52358157 100644 --- a/tests/test_functions/typed_events/typed_event.py +++ b/tests/test_functions/typed_events/typed_event.py @@ -62,7 +62,7 @@ def to_dict(self) -> dict: def function_typed(testType: TestType): valid_event = testType.name == "john" and testType.age == 10 if not valid_event: - flask.abort(500) + raise Exception("Received invalid input") return testType @@ -70,20 +70,20 @@ def function_typed(testType: TestType): def function_typed_reflect(testType: TestType): valid_event = testType.name == "jane" and testType.age == 20 if not valid_event: - flask.abort(500) + raise Exception("Received invalid input") return testType @functions_framework.typed -def function_typed_noreturn(testType: TestType): +def function_typed_no_return(testType: TestType): valid_event = testType.name == "jane" and testType.age == 20 if not valid_event: - flask.abort(500) + raise Exception("Received invalid input") @functions_framework.typed def function_typed_string_return(testType: TestType): valid_event = testType.name == "jane" and testType.age == 20 if not valid_event: - flask.abort(500) + raise Exception("Received invalid input") return "Hello " + testType.name diff --git a/tests/test_typed_event_functions.py b/tests/test_typed_event_functions.py index 227bb10a..a2c3d892 100644 --- a/tests/test_typed_event_functions.py +++ b/tests/test_typed_event_functions.py @@ -55,8 +55,8 @@ def test_typed_reflect_decorator(typed_decorator_client): assert resp.data == b'{"name": "jane", "age": 20}' -@pytest.mark.parametrize("function_name", ["function_typed_noreturn"]) -def test_typed_noreturn(typed_decorator_client): +@pytest.mark.parametrize("function_name", ["function_typed_no_return"]) +def test_typed_no_return(typed_decorator_client): resp = typed_decorator_client.post("/", json={"name": "jane", "age": 20}) assert resp.status_code == 200 assert resp.data == b"" @@ -89,12 +89,14 @@ def test_missing_type_information_typed_decorator(): with pytest.raises(FunctionsFrameworkException): create_app(target, source).test_client() + def test_missing_parameter_typed_decorator(): source = TEST_FUNCTIONS_DIR / "typed_events" / "missing_parameter.py" target = "function_typed_missing_parameter" with pytest.raises(FunctionsFrameworkException): create_app(target, source).test_client() + def test_missing_to_dict_typed_decorator(typed_decorator_missing_todict): resp = typed_decorator_missing_todict.post("/", json={"name": "john", "age": 10}) assert resp.status_code == 500 From 7dbbfedbaedd6e2537aa95ca5f50b058d7c38ecf Mon Sep 17 00:00:00 2001 From: Pratiksha Kap Date: Thu, 8 Dec 2022 12:23:59 -0800 Subject: [PATCH 15/16] add more tests --- .../typed_events/typed_event.py | 52 +++++++++++++++++++ tests/test_typed_event_functions.py | 27 ++++++++-- 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/tests/test_functions/typed_events/typed_event.py b/tests/test_functions/typed_events/typed_event.py index 52358157..ac00d2fe 100644 --- a/tests/test_functions/typed_events/typed_event.py +++ b/tests/test_functions/typed_events/typed_event.py @@ -58,6 +58,42 @@ def to_dict(self) -> dict: return result +class SampleType: + country: str + population: int + + def __init__(self, country: str, population: int) -> None: + self.country = country + self.population = population + + @staticmethod + def from_dict(obj: dict) -> "SampleType": + country = from_str(obj.get("country")) + population = from_int(obj.get("population")) + return SampleType(country, population) + + def to_dict(self) -> dict: + result: dict = {} + result["country"] = from_str(self.country) + result["population"] = from_int(self.population) + return result + + +class FaultyType: + country: str + population: int + + def __init__(self, country: str, population: int) -> None: + self.country = country + self.population = population + + @staticmethod + def from_dict(obj: dict) -> "SampleType": + country = from_str(obj.get("country")) + population = from_int(obj.get("population")) + return SampleType(country, population / 0) + + @functions_framework.typed(TestType) def function_typed(testType: TestType): valid_event = testType.name == "john" and testType.age == 10 @@ -87,3 +123,19 @@ def function_typed_string_return(testType: TestType): if not valid_event: raise Exception("Received invalid input") return "Hello " + testType.name + + +@functions_framework.typed(TestType) +def function_typed_different_types(testType: TestType) -> SampleType: + valid_event = testType.name == "jane" and testType.age == 20 + if not valid_event: + raise Exception("Received invalid input") + sampleType = SampleType("Monaco", 40000) + return sampleType + + +@functions_framework.typed +def function_typed_faulty_from_dict(input: FaultyType): + valid_event = input.country == "Monaco" and input.population == 40000 + if not valid_event: + raise Exception("Received invalid input") diff --git a/tests/test_typed_event_functions.py b/tests/test_typed_event_functions.py index a2c3d892..3b8d5da1 100644 --- a/tests/test_typed_event_functions.py +++ b/tests/test_typed_event_functions.py @@ -35,7 +35,7 @@ def typed_decorator_client(function_name): @pytest.fixture -def typed_decorator_missing_todict(): +def typed_decorator_missing_to_dict(): source = TEST_FUNCTIONS_DIR / "typed_events" / "missing_to_dict.py" target = "function_typed_missing_to_dict" return create_app(target, source).test_client() @@ -48,6 +48,20 @@ def test_typed_decorator(typed_decorator_client): assert resp.data == b'{"name": "john", "age": 10}' +@pytest.mark.parametrize("function_name", ["function_typed"]) +def test_typed_malformed_json(typed_decorator_client): + resp = typed_decorator_client.post("/", data="abc", content_type="application/json") + assert resp.status_code == 500 + + +@pytest.mark.parametrize("function_name", ["function_typed_faulty_from_dict"]) +def test_typed_faulty_from_dict(typed_decorator_client): + resp = typed_decorator_client.post( + "/", json={"country": "Monaco", "population": 40000} + ) + assert resp.status_code == 500 + + @pytest.mark.parametrize("function_name", ["function_typed_reflect"]) def test_typed_reflect_decorator(typed_decorator_client): resp = typed_decorator_client.post("/", json={"name": "jane", "age": 20}) @@ -55,6 +69,13 @@ def test_typed_reflect_decorator(typed_decorator_client): assert resp.data == b'{"name": "jane", "age": 20}' +@pytest.mark.parametrize("function_name", ["function_typed_different_types"]) +def test_typed_different_types(typed_decorator_client): + resp = typed_decorator_client.post("/", json={"name": "jane", "age": 20}) + assert resp.status_code == 200 + assert resp.data == b'{"country": "Monaco", "population": 40000}' + + @pytest.mark.parametrize("function_name", ["function_typed_no_return"]) def test_typed_no_return(typed_decorator_client): resp = typed_decorator_client.post("/", json={"name": "jane", "age": 20}) @@ -97,6 +118,6 @@ def test_missing_parameter_typed_decorator(): create_app(target, source).test_client() -def test_missing_to_dict_typed_decorator(typed_decorator_missing_todict): - resp = typed_decorator_missing_todict.post("/", json={"name": "john", "age": 10}) +def test_missing_to_dict_typed_decorator(typed_decorator_missing_to_dict): + resp = typed_decorator_missing_to_dict.post("/", json={"name": "john", "age": 10}) assert resp.status_code == 500 From 1265c123aa0d34b7e87864617ca206068c1408ba Mon Sep 17 00:00:00 2001 From: Pratiksha Kap Date: Mon, 12 Dec 2022 02:11:13 -0800 Subject: [PATCH 16/16] add comments --- src/functions_framework/__init__.py | 15 ++++++++++++--- src/functions_framework/_typed_event.py | 6 +++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index 2cb53797..c2a52d74 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -74,7 +74,7 @@ def wrapper(*args, **kwargs): def typed(*args): def _typed(func): - _typed_event.register_typed_event(google_type, func) + _typed_event.register_typed_event(input_type, func) @functools.wraps(func) def wrapper(*args, **kwargs): @@ -82,11 +82,20 @@ def wrapper(*args, **kwargs): return wrapper + # no input type provided as a parameter, we need to use reflection + # e.g function declaration: + # @typed + # def myfunc(x:input_type) if len(args) == 1 and isinstance(args[0], types.FunctionType): - google_type = "" + input_type = None return _typed(args[0]) + + # input type provided as a parameter to the decorator + # e.g. function declaration + # @typed(input_type) + # def myfunc(x) else: - google_type = args[0] + input_type = args[0] return _typed diff --git a/src/functions_framework/_typed_event.py b/src/functions_framework/_typed_event.py index 2f638665..40e715ae 100644 --- a/src/functions_framework/_typed_event.py +++ b/src/functions_framework/_typed_event.py @@ -70,13 +70,13 @@ def _validate_return_type(response): def _select_input_type(decorator_type, annotation_type): - if decorator_type == "" and annotation_type is inspect._empty: + if decorator_type == None and annotation_type is inspect._empty: raise TypeError( "The function defined does not contain Type of the input object." ) if ( - decorator_type != "" + decorator_type != None and annotation_type is not inspect._empty and decorator_type != annotation_type ): @@ -87,7 +87,7 @@ def _select_input_type(decorator_type, annotation_type): ) ) - if decorator_type == "": + if decorator_type == None: return annotation_type return decorator_type