diff --git a/Dockerfile b/Dockerfile index 8d326af..d576021 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,8 @@ FROM python:3.6.2 RUN mkdir /code ADD . /code/ -RUN pip install -e /code/ +RUN pip3 install -r /code/requirements.txt +RUN pip3 install -e /code/ WORKDIR /code/fdk/tests/fn/traceback ENTRYPOINT ["python3", "func.py"] diff --git a/README.md b/README.md index dbd4534..115e16e 100644 --- a/README.md +++ b/README.md @@ -14,14 +14,14 @@ In order to utilise this, you can write your `app.py` as follows: ```python import fdk -from fdk.http import response +from fdk import response def handler(context, data=None, loop=None): return response.RawResponse( - http_proto_version=context.version, - status_code=200, - headers={}, + context, + status_code=200, + headers={}, response_data=data.readall() ) @@ -31,7 +31,7 @@ if __name__ == "__main__": ``` -Automatic HTTP input coercions +Automatic input coercions ------------------------------ Decorators are provided that will attempt to coerce input values to Python types. @@ -40,7 +40,8 @@ Some attempt is made to coerce return values from these functions also: ```python import fdk -@fdk.coerce_http_input_to_content_type + +@fdk.coerce_input_to_content_type def handler(context, data=None, loop=None): """ body is a request body, it's type depends on content type @@ -53,7 +54,7 @@ if __name__ == "__main__": ``` -Working with async automatic HTTP input coercions +Working with async automatic input coercions ------------------------------------------------- Latest version supports async coroutines as a request body processors: @@ -61,16 +62,16 @@ Latest version supports async coroutines as a request body processors: import asyncio import fdk -from fdk.http import response +from fdk import response -@fdk.coerce_http_input_to_content_type +@fdk.coerce_input_to_content_type async def handler(context, data=None, loop=None): headers = { "Content-Type": "text/plain", } return response.RawResponse( - http_proto_version=context.version, + context, status_code=200, headers=headers, response_data="OK" @@ -82,7 +83,7 @@ if __name__ == "__main__": fdk.handle(handler, loop=loop) ``` -As you can see `app` function is no longer callable, because its type: coroutine, so we need to bypass event loop inside +As you can see `app` function is no longer callable, because its type: coroutine, so we need to bypass event loop inside Handling Hot JSON Functions --------------------------- @@ -126,7 +127,7 @@ if __name__ == "__main__": Applications powered by Fn: Concept ----------------------------------- -FDK is not only about developing functions, but providing necessary API to build serverless applications +FDK is not only about developing functions, but providing necessary API to build serverless applications that look like nothing but classes with methods powered by Fn. ```python @@ -163,6 +164,7 @@ class Application(object): r.raise_for_status() return r.text + if __name__ == "__main__": app = Application(config={}) @@ -190,7 +192,7 @@ if __name__ == "__main__": In order to identify to which Fn instance code needs to talk set following env var: ```bash - export API_URL=http://localhost:8080 + export API_URL = http: // localhost: 8080 ``` with respect to IP address or domain name where Fn lives. @@ -198,7 +200,7 @@ with respect to IP address or domain name where Fn lives. Applications powered by Fn: supply data to a function ----------------------------------------------------- -At this moment those helper-decorators let developers interact with Fn-powered functions as with regular class methods. +At this moment those helper - decorators let developers interact with Fn - powered functions as with regular class methods. In order to pass necessary data into a function developer just needs to do following ```python @@ -208,13 +210,13 @@ if __name__ == "__main__": app.env(keyone="blah", keytwo="blah", somethingelse=3) ``` -Key-value args will be turned into JSON instance and will be sent to a function as payload body. +Key - value args will be turned into JSON instance and will be sent to a function as payload body. Applications powered by Fn: working with function's result ---------------------------------------------------------- -In order to work with result from function you just need to read key-value argument `fn_data`: +In order to work with result from function you just need to read key - value argument `fn_data`: ```python @decorators.with_fn(fn_image="denismakogon/py-traceback-test:0.0.1", fn_format="http") @@ -225,7 +227,7 @@ In order to work with result from function you just need to read key-value argum Applications powered by Fn: advanced serverless functions --------------------------------------------------------- -Since release v0.0.3 developer can consume new API to build truly serverless functions +Since release v0.0.3 developer can consume new API to build truly serverless functions without taking care of Docker images, application, etc. ```python @@ -247,18 +249,18 @@ Each function decorated with `@decorator.fn` will become truly serverless and di So, how it works? * A developer writes function - * FDK (Fn-powered app) creates a recursive Pickle v4.0 with 3rd-party dependencies - * FDK (Fn-powered app) transfers pickled object to a function based on Python3 GPI (general purpose image) - * FDK unpickles function and its 3rd-party dependencies and runs it - * Function sends response back to Fn-powered application function caller + * FDK(Fn - powered app) creates a recursive Pickle v4.0 with 3rd - party dependencies + * FDK(Fn - powered app) transfers pickled object to a function based on Python3 GPI(general purpose image) + * FDK unpickles function and its 3rd - party dependencies and runs it + * Function sends response back to Fn - powered application function caller -So, each CPU-intensive functions can be sent to Fn with the only load on networking (given example creates 7kB of traffic between app's host and Fn). +So, each CPU - intensive functions can be sent to Fn with the only load on networking(given example creates 7kB of traffic between app's host and Fn). Applications powered by Fn: exceptions -------------------------------------- -Applications powered by Fn are following Go-like errors concept. It gives you full control on errors whether raise them or not. +Applications powered by Fn are following Go - like errors concept. It gives you full control on errors whether raise them or not. ```python res, err = app.env() if err: @@ -266,10 +268,10 @@ Applications powered by Fn are following Go-like errors concept. It gives you fu print(res) ``` -Each error is an instance fn `FnError` that encapsulates certain logic that makes hides HTTP errors and turns them into regular Python-like exceptions. +Each error is an instance fn `FnError` that encapsulates certain logic that makes hides HTTP errors and turns them into regular Python - like exceptions. TODOs ----- - - generic response class - - use fdk.headers.GoLikeHeaders in http + - generic response class + - use fdk.headers.GoLikeHeaders in http diff --git a/fdk/__init__.py b/fdk/__init__.py index 6e4e634..713645e 100644 --- a/fdk/__init__.py +++ b/fdk/__init__.py @@ -12,14 +12,57 @@ # License for the specific language governing permissions and limitations # under the License. -from fdk.http import handle as http_handler -from fdk import runner +import functools +import io +import ujson +from fdk import runner -coerce_http_input_to_content_type = http_handler.coerce_input_to_content_type handle = runner.generic_handle + +def coerce_input_to_content_type(request_data_processor): + + @functools.wraps(request_data_processor) + def app(context, data=None, loop=None): + """ + Request handler app dispatcher decorator + :param context: request context + :type context: request.RequestContext + :param data: request body + :type data: io.BufferedIOBase + :param loop: asyncio event loop + :type loop: asyncio.AbstractEventLoop + :return: raw response + :rtype: response.RawResponse + :return: + """ + body = data + content_type = context.Headers().get("content-type") + try: + + if hasattr(data, "readable"): + request_body = io.TextIOWrapper(data) + else: + request_body = data + + if content_type == "application/json": + if isinstance(request_body, str): + body = ujson.loads(request_body) + else: + body = ujson.load(request_body) + elif content_type in ["text/plain"]: + body = request_body.read() + + except Exception as ex: + raise context.DispatchError( + context, 500, "Unexpected error: {}".format(str(ex))) + + return request_data_processor(context, data=body, loop=loop) + + return app + + __all__ = [ - 'coerce_http_input_to_content_type', 'handle' ] diff --git a/fdk/context.py b/fdk/context.py index 224b031..dde6d6d 100644 --- a/fdk/context.py +++ b/fdk/context.py @@ -12,29 +12,78 @@ # License for the specific language governing permissions and limitations # under the License. +from fdk import errors + class RequestContext(object): - def __init__(self, method=None, url=None, - query_parameters=None, headers=None, - version=None): + def __init__(self, app_name, route, call_id, + fntype, config=None, headers=None, arguments=None): """ Request context here to be a placeholder for request-specific attributes - :param method: HTTP request method - :type method: str - :param url: HTTP request URL - :type url: str - :param query_parameters: HTTP request query parameters - :type query_parameters: dict - :param headers: HTTP request headers - :type headers: object - :param version: HTTP proto version - :type version: tuple """ - # TODO(xxx): app name, path, memory, type, config - self.method = method - self.url = url - self.query_parameters = query_parameters - self.headers = headers - self.version = version + self.__app_name = app_name + self.__app_route = route + self.__call_id = call_id + self.__config = config if config else {} + self.__headers = headers if headers else {} + self.__arguments = {} if not arguments else arguments + self.__type = fntype + + def AppName(self): + return self.__app_name + + def Route(self): + return self.__app_route + + def CallID(self): + return self.__call_id + + def Config(self): + return self.__config + + def Headers(self): + return self.__headers + + def Arguments(self): + return self.__arguments + + def Type(self): + return self.__type + + +class HTTPContext(RequestContext): + + def __init__(self, app_name, route, + call_id, fntype="http", + config=None, headers=None, + method=None, url=None, + query_parameters=None, + version=None): + arguments = { + "method": method, + "URL": url, + "query": query_parameters, + "http_version": version + } + self.DispatchError = errors.HTTPDispatchException + super(HTTPContext, self).__init__( + app_name, route, call_id, fntype, + config=config, headers=headers, arguments=arguments) + + +class JSONContext(RequestContext): + + def __init__(self, app_name, route, call_id, + fntype="json", config=None, headers=None): + self.DispatchError = errors.JSONDispatchException + super(JSONContext, self).__init__( + app_name, route, call_id, fntype, config=config, headers=headers) + + +def fromType(fntype, *args, **kwargs): + if fntype == "json": + return JSONContext(*args, **kwargs) + if fntype == "http": + return HTTPContext(*args, **kwargs) diff --git a/fdk/errors.py b/fdk/errors.py index 925b6a8..afd0e8c 100644 --- a/fdk/errors.py +++ b/fdk/errors.py @@ -13,13 +13,12 @@ # under the License. from fdk import headers -from fdk.http import response as http_response -from fdk.json import response as json_response +from fdk import response class HTTPDispatchException(Exception): - def __init__(self, status, message): + def __init__(self, context, status, message): """ HTTP response with error :param status: HTTP status code @@ -27,9 +26,11 @@ def __init__(self, status, message): """ self.status = status self.message = message + self.context = context def response(self): - return http_response.RawResponse( + return response.RawResponse( + self.context, status_code=self.status, headers={ "content-type": "text/plain" @@ -39,7 +40,7 @@ def response(self): class JSONDispatchException(Exception): - def __init__(self, status, message): + def __init__(self, context, status, message): """ JSON response with error :param status: HTTP status code @@ -47,14 +48,17 @@ def __init__(self, status, message): """ self.status = status self.message = message + self.context = context def response(self): resp_headers = headers.GoLikeHeaders({}) resp_headers.set("content-type", "text/plain; charset=utf-8") - return json_response.RawResponse( + return response.RawResponse( + self.context, response_data={ "error": { "message": self.message, } }, - headers=resp_headers, status_code=500) + headers=resp_headers, + status_code=self.status) diff --git a/fdk/headers.py b/fdk/headers.py index c365068..2edc617 100644 --- a/fdk/headers.py +++ b/fdk/headers.py @@ -25,13 +25,14 @@ def __init__(self, headers): .format(type(headers))) for k, v in headers.copy().items(): del headers[k] - headers[k.lower().replace("fn_header_", "").replace('_', '-')] = v + headers[k.lower().replace("fn_header_", "")] = v self.__headers = headers - def get(self, key): + def get(self, key, default=None): """ :param key: + :param default: :return: """ if key in self.__headers: @@ -39,7 +40,7 @@ def get(self, key): if len(self.__headers[key]) == 1 else self.__headers[key]) else: - raise KeyError("Missing key: {}".format(key)) + return default def set(self, key, value): """ diff --git a/fdk/http/handle.py b/fdk/http/handle.py index 6d74275..c738c08 100644 --- a/fdk/http/handle.py +++ b/fdk/http/handle.py @@ -12,15 +12,13 @@ # License for the specific language governing permissions and limitations # under the License. -import functools -import io -import json import types +import ujson -from fdk import errors -from fdk.http import response +from fdk import response +@response.safe def normal_dispatch(app, context, data=None, loop=None): """ Request handler app dispatcher @@ -35,71 +33,26 @@ def normal_dispatch(app, context, data=None, loop=None): :return: raw response :rtype: response.RawResponse """ - try: - rs = app(context, data=data, loop=loop) - if isinstance(rs, response.RawResponse): - return rs - elif isinstance(rs, types.CoroutineType): - return loop.run_until_complete(rs) - elif isinstance(rs, str): - return response.RawResponse(http_proto_version=context.version, - status_code=200, - headers={}, - response_data=rs) - elif isinstance(rs, bytes): - return response.RawResponse( - http_proto_version=context.version, - status_code=200, - headers={'content-type': 'application/octet-stream'}, - response_data=rs.decode("utf8")) - else: - return response.RawResponse( - http_proto_version=context.version, - status_code=200, - headers={'content-type': 'application/json'}, - response_data=json.dumps(rs)) - except errors.HTTPDispatchException as e: - return e.response() - except Exception as e: + rs = app(context, data=data, loop=loop) + if isinstance(rs, response.RawResponse): + return rs + elif isinstance(rs, types.CoroutineType): + return loop.run_until_complete(rs) + elif isinstance(rs, str): return response.RawResponse( - http_proto_version=context.version, - status_code=500, + context, + status_code=200, headers={}, - response_data=str(e)) - - -def coerce_input_to_content_type(request_data_processor): - - @functools.wraps(request_data_processor) - def app(context, data=None, loop=None): - """ - Request handler app dispatcher decorator - :param context: request context - :type context: request.RequestContext - :param data: request body - :type data: io.BufferedIOBase - :param loop: asyncio event loop - :type loop: asyncio.AbstractEventLoop - :return: raw response - :rtype: response.RawResponse - :return: - """ - # TODO(jang): The content-type header has some internal structure; - # actually provide some parsing for that - content_type = context.headers.get("content-type") - try: - request_body = io.TextIOWrapper(data) - # TODO(denismakogon): XML type to add - if content_type == "application/json": - body = json.load(request_body) - elif content_type in ["text/plain"]: - body = request_body.read() - else: - body = request_body.read() - except Exception as ex: - raise errors.HTTPDispatchException( - 500, "Unexpected error: {}".format(str(ex))) - - return request_data_processor(context, data=body, loop=loop) - - return app + response_data=rs) + elif isinstance(rs, bytes): + return response.RawResponse( + context, + status_code=200, + headers={'content-type': 'application/octet-stream'}, + response_data=rs.decode("utf8")) + else: + return response.RawResponse( + context, + status_code=200, + headers={'content-type': 'application/json'}, + response_data=ujson.dumps(rs)) diff --git a/fdk/http/request.py b/fdk/http/request.py index 44572d8..806e02d 100644 --- a/fdk/http/request.py +++ b/fdk/http/request.py @@ -12,38 +12,13 @@ # License for the specific language governing permissions and limitations # under the License. +import os import urllib.parse +from fdk import context from fdk import errors -class RequestContext(object): - - def __init__(self, method=None, url=None, - query_parameters=None, headers=None, - version=None): - """ - Request context here to be a placeholder - for request-specific attributes - :param method: HTTP request method - :type method: str - :param url: HTTP request URL - :type url: str - :param query_parameters: HTTP request query parameters - :type query_parameters: dict - :param headers: HTTP request headers - :type headers: dict - :param version: HTTP proto version - :type version: tuple - """ - # TODO(xxx): app name, path, memory, type, config - self.method = method - self.url = url - self.query_parameters = query_parameters - self.headers = headers - self.version = version - - def readline(stream): """Read a line up until the \r\n termination @@ -160,16 +135,24 @@ def parse_raw_request(self): self.body_stream = self.stream self.stream = None - context = RequestContext( + ctx = context.HTTPContext( + os.environ.get("FN_APP_NAME"), + os.environ.get("FN_PATH"), + headers.get('fn_call_id'), + config=os.environ, method=method, url=path, query_parameters=params, headers=headers, version=(major, minor)) - return context, self.body_stream + return ctx, self.body_stream except ValueError: - raise errors.HTTPDispatchException(500, "No request supplied") + ctx = context.HTTPContext( + os.environ.get("FN_APP_NAME"), + os.environ.get("FN_PATH"), "", + ) + raise errors.HTTPDispatchException(ctx, 500, "No request supplied") def parse_query_params(url): @@ -334,6 +317,7 @@ class ChunkedStream(object): chunk is a regular chunk, with the exception that its length is zero. """ + def __init__(self, base_stream): """ Chunked stream reader diff --git a/fdk/http/response.py b/fdk/http/response.py index bf1e00d..4b20e26 100644 --- a/fdk/http/response.py +++ b/fdk/http/response.py @@ -15,7 +15,7 @@ from fdk import statuses -class RawResponse(object): +class HTTPResponse(object): PATTERN = ("HTTP/{proto_major}.{proto_minor} " "{int_status} {verbose_status}\r\n" "{headers}") @@ -36,7 +36,6 @@ def __init__(self, http_proto_version=(1, 1), status_code=200, http_headers = headers if headers else {} self.http_proto = http_proto_version self.status_code = status_code - self.verbose_status = statuses.from_code(status_code) self.response_data, content_len = self.__encode_data(response_data) if self.response_data: if not http_headers.get("Content-Type"): @@ -78,7 +77,7 @@ def dump(self, stream, flush=True): "proto_major": self.http_proto[0], "proto_minor": self.http_proto[1], "int_status": self.status_code, - "verbose_status": self.verbose_status, + "verbose_status": statuses.from_code(self.status_code), "headers": self.__encode_headers(self.headers), } result = stream.write( diff --git a/fdk/json/handle.py b/fdk/json/handle.py index b293728..2c6c033 100644 --- a/fdk/json/handle.py +++ b/fdk/json/handle.py @@ -15,11 +15,11 @@ import types import ujson -from fdk import errors from fdk import headers -from fdk.json import response +from fdk import response +@response.safe def normal_dispatch(app, context, data=None, loop=None): """ Request handler app dispatcher @@ -34,33 +34,30 @@ def normal_dispatch(app, context, data=None, loop=None): :return: raw response :rtype: response.RawResponse """ - try: - rs = app(context, data=data, loop=loop) - if isinstance(rs, response.RawResponse): - return rs - elif isinstance(rs, types.CoroutineType): - return loop.run_until_complete(rs) - elif isinstance(rs, str): - hs = headers.GoLikeHeaders({}) - hs.set('content-type', 'text/plain') - return response.RawResponse(response_data=rs) - elif isinstance(rs, bytes): - hs = headers.GoLikeHeaders({}) - hs.set('content-type', 'application/octet-stream') - return response.RawResponse( - response_data=rs.decode("utf8"), - headers=hs, - status_code=200 - ) - else: - hs = headers.GoLikeHeaders({}) - hs.set('content-type', 'application/json') - return response.RawResponse( - ujson.dumps(rs), - headers=hs, - status_code=200, - ) - except errors.JSONDispatchException as e: - return e.response() - except Exception as ex: - return errors.JSONDispatchException(500, str(ex)).response() + rs = app(context, data=data, loop=loop) + if isinstance(rs, response.RawResponse): + return rs + elif isinstance(rs, types.CoroutineType): + return loop.run_until_complete(rs) + elif isinstance(rs, str): + hs = headers.GoLikeHeaders({}) + hs.set('content-type', 'text/plain') + return response.RawResponse(context, response_data=rs) + elif isinstance(rs, bytes): + hs = headers.GoLikeHeaders({}) + hs.set('content-type', 'application/octet-stream') + return response.RawResponse( + context, + response_data=rs.decode("utf8"), + headers=hs, + status_code=200 + ) + else: + hs = headers.GoLikeHeaders({}) + hs.set('content-type', 'application/json') + return response.RawResponse( + context, + response_data=ujson.dumps(rs), + headers=hs, + status_code=200, + ) diff --git a/fdk/json/request.py b/fdk/json/request.py index cf3638b..561221d 100644 --- a/fdk/json/request.py +++ b/fdk/json/request.py @@ -81,15 +81,18 @@ def parse_raw_request(self): incoming_json = readline(self.stream) print("After JSON parsing: {}".format(incoming_json), file=sys.stderr, flush=True) - request_context = context.RequestContext( - method=os.environ.get("FN_METHOD"), - url=os.environ.get("FN_REQUEST_URL"), - query_parameters={}, - headers=headers.GoLikeHeaders( - incoming_json.get('headers', {})) - ) - return request_context, incoming_json.get('body') + json_headers = headers.GoLikeHeaders( + incoming_json.get('protocol', {"headers": {}}).get('headers')) + ctx = context.JSONContext(os.environ.get("FN_APP_NAME"), + os.environ.get("FN_PATH"), + incoming_json.get("call_id"), + config=os.environ, headers=json_headers) + return ctx, incoming_json.get('body') except Exception as ex: print("Error while parsing JSON: {}".format(str(ex)), file=sys.stderr, flush=True) - raise errors.JSONDispatchException(500, str(ex)) + ctx = context.JSONContext( + os.environ.get("FN_APP_NAME"), + os.environ.get("FN_PATH"), "", + ) + raise errors.JSONDispatchException(ctx, 500, str(ex)) diff --git a/fdk/json/response.py b/fdk/json/response.py index 1e76acb..a96a146 100644 --- a/fdk/json/response.py +++ b/fdk/json/response.py @@ -15,10 +15,10 @@ import sys import ujson -from fdk import headers as http_headers +from fdk import headers as rh -class RawResponse(object): +class JSONResponse(object): def __init__(self, response_data=None, headers=None, status_code=200): """ @@ -31,19 +31,12 @@ def __init__(self, response_data=None, headers=None, status_code=200): :type status_code: int """ self.status_code = status_code - - if isinstance(response_data, dict): - self.body = response_data if response_data else {} - if isinstance(response_data, str): - self.body = response_data if response_data else "" - - if headers: - if not isinstance(headers, http_headers.GoLikeHeaders): - raise TypeError("headers should be of " - "`hotfn.headers.GoLikeHeaders` type!") + self.response_data = ujson.dumps(response_data) + self.headers = rh.GoLikeHeaders({}) + if isinstance(headers, dict): + self.headers = rh.GoLikeHeaders(headers) + if isinstance(headers, rh.GoLikeHeaders): self.headers = headers - else: - self.headers = http_headers.GoLikeHeaders({}) def dump(self, stream, flush=True): """ @@ -52,10 +45,9 @@ def dump(self, stream, flush=True): :param flush: whether flush data on write or not :return: result of dumping """ - raw_body = ujson.dumps(self.body) - self.headers.set("content-length", len(raw_body)) + self.headers.set("content-length", len(self.response_data)) resp = ujson.dumps({ - "body": raw_body, + "body": self.response_data, "status_code": self.status_code, "headers": self.headers.for_dump() }) diff --git a/fdk/response.py b/fdk/response.py new file mode 100644 index 0000000..fb4027b --- /dev/null +++ b/fdk/response.py @@ -0,0 +1,71 @@ +# All Rights Reserved. +# +# 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 functools +import traceback +import sys + +from fdk.http import response as hr +from fdk.json import response as jr + + +def safe(dispatcher): + + @functools.wraps(dispatcher) + def wrapper(app, context, data=None, loop=None): + try: + return dispatcher(app, context, data=data, loop=loop) + except (Exception, TimeoutError) as ex: + traceback.print_exc(file=sys.stderr) + status = 502 if isinstance(ex, TimeoutError) else 500 + return context.DispatchError(context, status, str(ex)).response() + + return wrapper + + +class RawResponse(object): + + def __init__(self, context, response_data=None, + headers=None, status_code=200): + """ + Generic response object + :param context: request context + :type context: fdk.context.RequestContext + :param response_data: response data + :type response_data: object + :param headers: response headers + :param status_code: status code + :type status_code: int + """ + if context.Type() == "json": + self.response = jr.JSONResponse( + response_data=response_data, + headers=headers, + status_code=status_code) + if context.Type() == "http": + self.response = hr.HTTPResponse( + status_code=status_code, + response_data=str(response_data), + headers=headers, + http_proto_version=context.Arguments().get( + "http_version")) + + def status(self): + return self.response.status_code + + def body(self): + return self.response.response_data + + def dump(self, stream, flush=True): + return self.response.dump(stream, flush=flush) diff --git a/fdk/runner.py b/fdk/runner.py index 1afc6c6..f6dc0fd 100644 --- a/fdk/runner.py +++ b/fdk/runner.py @@ -12,12 +12,14 @@ # License for the specific language governing permissions and limitations # under the License. +import contextlib +import datetime as dt +import iso8601 import os +import signal import sys -import traceback - -from fdk import errors +from fdk import context from fdk.http import handle as http_handle from fdk.http import request as http_request from fdk.json import handle as json_handle @@ -33,28 +35,26 @@ def generic_handle(handler, loop=None): :type loop: asyncio.AbstractEventLoop :return: None """ - (exc_class, request_class, + fn_format = os.environ.get("FN_FORMAT") + (request_class, stream_reader_mode, stream_writer_mode, dispatcher) = ( - None, None, "rb", "wb", None) + None, "rb", "wb", None) - fn_format = os.environ.get("FN_FORMAT") + if fn_format in [None, "default"]: + exit(501) if fn_format == "json": - (exc_class, - request_class, + (request_class, stream_reader_mode, stream_writer_mode, dispatcher) = ( - errors.JSONDispatchException, json_request.RawRequest, "r", "w", json_handle.normal_dispatch) if fn_format == "http": - (exc_class, - request_class, + (request_class, stream_reader_mode, stream_writer_mode, dispatcher) = ( - errors.HTTPDispatchException, http_request.RawRequest, "rb", "wb", http_handle.normal_dispatch) @@ -66,31 +66,54 @@ def generic_handle(handler, loop=None): request = request_class(read_stream) while True: proceed_with_streams(handler, request, write_stream, - dispatcher, exc_class, loop=loop) + dispatcher, loop=loop) + + +@contextlib.contextmanager +def timeout(request, write_stream): + + def handler(*_): + raise TimeoutError("Function timed out") + + fn_format = os.environ.get("FN_FORMAT") + ctx = context.fromType(fn_format, + os.environ.get("FN_APP_NAME"), + os.environ.get("FN_PATH"), "",) + + try: + ctx, data = request.parse_raw_request() + + deadline = ctx.Headers().get("fn_deadline") + alarm_after = iso8601.parse_date(deadline) + now = dt.datetime.now(dt.timezone.utc).astimezone() + delta = alarm_after - now + signal.signal(signal.SIGALRM, handler) + signal.alarm(int(delta.total_seconds())) + + yield (ctx, data) + + except EOFError: + # pipe closed from the other side by Fn + return + except ctx.DispatchError as ex: + signal.alarm(0) + ex.response().dump(write_stream) + return def proceed_with_streams(handler, request, write_stream, - dispatcher, exc_class, loop=None): + dispatcher, loop=None): """ Handles both request parsing and dumping :param handler: request body handler :param request: incoming request :param write_stream: write stream (usually STDOUT) :param dispatcher: raw HTTP/JSON request dispatcher - :param exc_class: HTTP/JSON exception class :param loop: asyncio event loop :return: """ - try: - context, data = request.parse_raw_request() - rs = dispatcher(handler, context, + + with timeout(request, write_stream) as (ctx, data): + rs = dispatcher(handler, ctx, data=data, loop=loop) rs.dump(write_stream) - except EOFError: - # pipe closed from the other side by Fn - return - except exc_class as ex: - ex.response().dump(write_stream) - except Exception as ex: - traceback.print_exc(file=sys.stderr) - exc_class(500, str(ex)).response().dump(write_stream) diff --git a/fdk/tests/data.py b/fdk/tests/data.py index b576c87..0ec014a 100644 --- a/fdk/tests/data.py +++ b/fdk/tests/data.py @@ -35,13 +35,34 @@ Accept: */*\r """ +http_coerce = """POST /r/emokognition/detect HTTP/1.1\r +Host: localhost:9000\r +Fn_header_accept: */*\r +Fn_header_content_length: 46\r +Fn_header_content_type: application/json\r +Fn_header_user_agent: curl/7.54.0\r +\r +{"media_url": "http://localhost:8080/img.png"} +""" + http_request_with_fn_content_headers = """POST /r/emokognition/detect HTTP/1.1\r Host: localhost:9000\r Fn_header_accept: */*\r Fn_header_content_length: 46\r Fn_header_content_type: application/x-www-form-urlencoded\r Fn_header_user_agent: curl/7.54.0\r -\r\n{"media_url": "http://localhost:8080/img.png"} +\r +{"media_url": "http://localhost:8080/img.png"} +""" + +http_with_deadline = """GET /v1/apps?something=something&etc=etc HTTP/1.1\r +Host: localhost:8080\r +Content-Length: 11\r +Content-Type: application/x-www-form-urlencoded\r +User-Agent: curl/7.51.0\r +Fn_deadline: {}\r +\r +hello:hello """ json_request_with_data = ( @@ -52,7 +73,7 @@ ',"request_url":"/v1/apps?something=something&etc=etc"\n' ',"headers":{"Content-Type":["application/json"],' '"Host":["localhost:8080"],"User-Agent":["curl/7.51.0"]}\n' - '\n}\n\n') + '\n}\n}\n\n') json_request_without_data = ( '{\n"call_id":"some_id"\n,' @@ -62,4 +83,14 @@ ',"request_url":"/v1/apps?something=something&etc=etc"\n' ',"headers":{"Content-Type":["application/json"],' '"Host":["localhost:8080"],"User-Agent":["curl/7.51.0"]}\n' - '\n}\n\n') + '\n}\n}\n\n') + +json_with_deadline = ( + '{\n"call_id":"some_id"\n,' + '"content_type":"application/json"\n' + ',"body":"{\\"a\\":\\"a\\"}\n"\n' + ',"protocol":{"type":"json"\n' + ',"request_url":"/v1/apps?something=something&etc=etc"\n' + ',"headers":{"Content-Type":["application/json"],' + '"Host":["localhost:8080"],' + '"User-Agent":["curl/7.51.0"],') diff --git a/fdk/tests/fn/traceback/Dockerfile b/fdk/tests/fn/traceback/Dockerfile index 6f70318..85e3255 100644 --- a/fdk/tests/fn/traceback/Dockerfile +++ b/fdk/tests/fn/traceback/Dockerfile @@ -4,7 +4,7 @@ FROM python:3.6.2 RUN mkdir /code ADD . /code/ WORKDIR /code/ -RUN pip install -r requirements.txt + WORKDIR /code/ ENTRYPOINT ["python3", "func.py"] diff --git a/fdk/tests/fn/traceback/func.py b/fdk/tests/fn/traceback/func.py index 81118bf..a96fd72 100644 --- a/fdk/tests/fn/traceback/func.py +++ b/fdk/tests/fn/traceback/func.py @@ -16,7 +16,7 @@ import sys import traceback -from fdk.http import response +from fdk import response def exception_to_string(err): @@ -27,11 +27,12 @@ def exception_to_string(err): '\n {} {}'.format(err.__class__, err)) +@fdk.coerce_input_to_content_type def handler(context, data=None, loop=None): """ This is just an echo function :param context: request context - :type context: hotfn.http.request.RequestContext + :type context: fdk.context.RequestContext :param data: request body :type data: object :param loop: asyncio event loop @@ -39,22 +40,17 @@ def handler(context, data=None, loop=None): :return: echo of request body :rtype: object """ - headers = { - "Content-Type": "text/plain", - } - rs = response.RawResponse( - http_proto_version=context.version, - status_code=200, - headers=headers, - response_data="OK" - ) print("response created", file=sys.stderr, flush=True) try: raise Exception("test-exception") except Exception as ex: print("exception raised", file=sys.stderr, flush=True) - rs.status_code = 500 - rs.set_response_content(exception_to_string(ex)) + rs = response.RawResponse( + context, + status_code=500, + headers={"Content-Type": "text/plain"}, + response_data=exception_to_string(ex) + ) return rs diff --git a/fdk/tests/fn/traceback/func.yaml b/fdk/tests/fn/traceback/func.yaml new file mode 100644 index 0000000..7e322e1 --- /dev/null +++ b/fdk/tests/fn/traceback/func.yaml @@ -0,0 +1,5 @@ +name: fnproject/fdk-traceback-test +version: 0.0.1 +runtime: docker +type: sync +format: json diff --git a/fdk/tests/fn/traceback/requirements.txt b/fdk/tests/fn/traceback/requirements.txt deleted file mode 100644 index f4e7be3..0000000 --- a/fdk/tests/fn/traceback/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -fdk-python diff --git a/fdk/tests/test_dispatcher.py b/fdk/tests/test_dispatcher.py new file mode 100644 index 0000000..aa6e2d1 --- /dev/null +++ b/fdk/tests/test_dispatcher.py @@ -0,0 +1,203 @@ +# All Rights Reserved. +# +# 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 datetime as dt +import fdk +import io +import os +import time + +import testtools + +from fdk.http import request as hr +from fdk.json import handle as jh +from fdk.http import handle as hh +from fdk.json import request as jr +from fdk import response +from fdk import runner +from fdk.tests import data + + +def handle(ctx, **kwargs): + pass + + +def sleeper(ctx, **kwargs): + time.sleep(12) + + +def custom_response(ctx, **kwargs): + return response.RawResponse( + ctx, + status_code=403, + headers={ + "Content-Type": "text/plain", + }, + response_data="Forbidden. Bad credentials.", + ) + + +def error(ctx, **kwargs): + raise Exception("custom error, should be handled") + + +# TODO(denismakogon): FUCK, fix this!!! +@fdk.coerce_input_to_content_type +def validate(ctx, **kwargs): + return response.RawResponse( + ctx, response_data=isinstance(kwargs.get("data"), dict), + headers={ + "Content-Type": "text/plain", + }, + status_code=200, + ) + + +class TestRequestTypeCoercing(testtools.TestCase): + + def setUp(self): + super(TestRequestTypeCoercing, self).setUp() + + def tearDown(self): + super(TestRequestTypeCoercing, self).tearDown() + + def test_http_data_coercing(self): + req = hr.RawRequest(io.BytesIO( + data.http_coerce.encode("utf8"))) + ctx, body = req.parse_raw_request() + resp = hh.normal_dispatch(validate, ctx, data=body) + self.assertEqual(200, resp.status()) + self.assertIn(b"True", resp.body()) + + def test_json_data_coercing(self): + req = jr.RawRequest(io.StringIO(data.json_request_with_data)) + ctx, body = req.parse_raw_request() + resp = jh.normal_dispatch(validate, ctx, data=body) + self.assertEqual(200, resp.status()) + self.assertIn("true", resp.body()) + + +class TestJSONDispatcher(testtools.TestCase): + + def setUp(self): + super(TestJSONDispatcher, self).setUp() + + def tearDown(self): + super(TestJSONDispatcher, self).tearDown() + + def run_json_func(self, func): + req = jr.RawRequest(io.StringIO(data.json_request_with_data)) + ctx, body = req.parse_raw_request() + return jh.normal_dispatch(func, ctx, data=body) + + def test_json_normal_dispatch(self): + resp = self.run_json_func(handle) + self.assertEqual(200, resp.status()) + + def test_json_custom_response(self): + resp = self.run_json_func(custom_response) + self.assertEqual(403, resp.status()) + + def test_json_custom_error(self): + resp = self.run_json_func(error) + self.assertEqual(500, resp.status()) + self.assertIn("custom error, should be handled", resp.body()) + + +class TestHTTPDispatcher(testtools.TestCase): + + def setUp(self): + super(TestHTTPDispatcher, self).setUp() + + def tearDown(self): + super(TestHTTPDispatcher, self).tearDown() + + def run_http_func(self, func): + req = hr.RawRequest(io.BytesIO( + data.http_request_with_query_and_data.encode("utf8"))) + ctx, body = req.parse_raw_request() + return hh.normal_dispatch(func, ctx, data=body) + + def test_http_normal_dispatch(self): + resp = self.run_http_func(handle) + self.assertEqual(200, resp.status()) + + def test_http_custom_response(self): + resp = self.run_http_func(custom_response) + self.assertEqual(403, resp.status()) + + def test_http_custom_error(self): + resp = self.run_http_func(error) + self.assertEqual(500, resp.status()) + self.assertIn(b"custom error, should be handled", resp.body()) + + +class TestJSONDeadline(testtools.TestCase): + + def setUp(self): + super(TestJSONDeadline, self).setUp() + + def tearDown(self): + super(TestJSONDeadline, self).tearDown() + + def run_json_func(self, func, deadile_is_seconds): + os.environ.setdefault("FN_FORMAT", "json") + now = dt.datetime.now(dt.timezone.utc).astimezone() + deadline = now + dt.timedelta(seconds=deadile_is_seconds) + r = (data.json_with_deadline + + '"Fn_deadline":["{}"]'.format( + deadline.isoformat()) + '}\n' + '}\n}\n\n') + req = jr.RawRequest(io.StringIO(r)) + write_stream = io.StringIO() + runner.proceed_with_streams( + func, req, write_stream, jh.normal_dispatch) + return write_stream.getvalue() + + def test_json_with_deadline(self): + raw = self.run_json_func(handle, 30) + self.assertIn('"status_code":200', raw) + + def test_json_timeout_by_deadline(self): + raw = self.run_json_func(sleeper, 10) + self.assertIn('"status_code":502', raw) + self.assertIn('Function timed out', raw) + + +class TestHTTPDeadline(testtools.TestCase): + def setUp(self): + super(TestHTTPDeadline, self).setUp() + + def tearDown(self): + super(TestHTTPDeadline, self).tearDown() + + def run_http_func(self, func, deadile_is_seconds): + os.environ.setdefault("FN_FORMAT", "http") + now = dt.datetime.now(dt.timezone.utc).astimezone() + deadline = now + dt.timedelta(seconds=deadile_is_seconds) + with_deadline = data.http_with_deadline.format(deadline.isoformat()) + req = hr.RawRequest( + io.BytesIO(with_deadline.encode("utf8"))) + write_stream = io.BytesIO(bytes()) + runner.proceed_with_streams( + func, req, write_stream, hh.normal_dispatch) + return write_stream.getvalue().decode("utf-8") + + def test_http_with_deadline(self): + raw = self.run_http_func(handle, 30) + self.assertIn("200 OK", raw) + + def test_http_timeout_by_deadline(self): + raw = self.run_http_func(sleeper, 10) + self.assertIn("502 Bad Gateway", raw) + self.assertIn("Function timed out", raw) diff --git a/fdk/tests/test_http_request_parser.py b/fdk/tests/test_http_request_parser.py index b9220d5..59d8715 100644 --- a/fdk/tests/test_http_request_parser.py +++ b/fdk/tests/test_http_request_parser.py @@ -32,43 +32,50 @@ def test_parse_no_data(self): req_parser = request.RawRequest( io.BytesIO(data.http_request_no_data.encode("utf8"))) context, request_data = req_parser.parse_raw_request() - self.assertEqual("GET", context.method) - self.assertIn("host", context.headers) - self.assertIn("accept", context.headers) - self.assertIn("user-agent", context.headers) - self.assertEqual(("1", "1"), context.version) + request_arguments = context.Arguments() + headers = context.Headers() + self.assertEqual("GET", request_arguments.get("method")) + self.assertIn("host", headers) + self.assertIn("accept", headers) + self.assertIn("user-agent", headers) + self.assertEqual(("1", "1"), request_arguments.get("http_version")) self.assertEqual(0, len(request_data.read())) - self.assertIn("something", context.query_parameters) + self.assertIn("something", request_arguments.get("query")) def test_parse_no_query(self): req_parser = request.RawRequest( io.BytesIO(data.http_request_no_query.encode("utf8"))) context, request_data = req_parser.parse_raw_request() - self.assertEqual("GET", context.method) - self.assertIn("host", context.headers) - self.assertIn("accept", context.headers) - self.assertIn("user-agent", context.headers) - self.assertEqual(("1", "1"), context.version) + request_arguments = context.Arguments() + headers = context.Headers() + self.assertEqual("GET", request_arguments.get("method")) + self.assertIn("host", headers) + self.assertIn("accept", headers) + self.assertIn("user-agent", headers) + self.assertEqual(("1", "1"), request_arguments.get("http_version")) self.assertEqual(0, len(request_data.read())) - self.assertEqual({}, context.query_parameters) + self.assertEqual({}, request_arguments.get("query")) def test_parse_data(self): req_parser = request.RawRequest(io.BytesIO( data.http_request_with_query_and_data.encode("utf8"))) context, request_data = req_parser.parse_raw_request() - self.assertEqual("GET", context.method) - self.assertIn("host", context.headers) - self.assertIn("content-type", context.headers) - self.assertIn("content-length", context.headers) - self.assertEqual("11", context.headers.get("content-length")) - self.assertIn("user-agent", context.headers) - self.assertEqual(("1", "1"), context.version) + request_arguments = context.Arguments() + headers = context.Headers() + self.assertEqual("GET", request_arguments.get("method")) + self.assertIn("host", headers) + self.assertIn("content-type", headers) + self.assertIn("content-length", headers) + self.assertEqual("11", headers.get("content-length")) + self.assertIn("user-agent", headers) + self.assertEqual(("1", "1"), request_arguments.get("http_version")) self.assertEqual("hello:hello", request_data.readall().decode()) - self.assertIn("something", context.query_parameters) + self.assertIn("something", request_arguments.get("query")) def test_parse_data_with_fn_content_length(self): req_parser = request.RawRequest(io.BytesIO( data.http_request_with_fn_content_headers.encode("utf8"))) context, request_data = req_parser.parse_raw_request() + headers = context.Headers() self.assertEqual(len(request_data.read()), - int(context.headers.get("content-length"))) + int(headers.get("content-length"))) diff --git a/fdk/tests/test_json_request_parser.py b/fdk/tests/test_json_request_parser.py index ba937d7..1359ab4 100644 --- a/fdk/tests/test_json_request_parser.py +++ b/fdk/tests/test_json_request_parser.py @@ -36,7 +36,7 @@ def test_parse_request_with_data(self): self.assertIsNotNone(context) self.assertIsNotNone(request_data) self.assertIsInstance(request_data, str) - self.assertIsInstance(context.headers, headers.GoLikeHeaders) + self.assertIsInstance(context.Headers(), headers.GoLikeHeaders) def test_parse_request_without_data(self): req_parser = request.RawRequest( diff --git a/release_doc.md b/release_doc.md index 5bb0704..99e55fd 100644 --- a/release_doc.md +++ b/release_doc.md @@ -4,24 +4,14 @@ Release process Run tests on target brunch -------------------------- -Steps:: +Steps: - tox -epep8 - tox -epy3.5 - tox -epy3.6 - - -Declare package version ------------------------ - -In setup.py bump version to the next:: - - version='X.X.X' to version='X.X.Y' + tox Cut off stable branch --------------------- -Steps:: +Steps: git checkout -b vX.X.X-stable git push origin vX.X.X-stable @@ -30,16 +20,16 @@ Steps:: Create GitHub tag ----------------- -Steps:: +Steps: Releases ---> Draft New Release - Name: HotFn version X.X.X stable release + Name: FDK-Python version X.X.X stable release Collect changes from previous version ------------------------------------- -Steps:: +Steps: git log --oneline --decorate @@ -47,7 +37,7 @@ Steps:: Build distribution package -------------------------- -Steps:: +Steps: PBR_VERSION=X.X.X python setup.py sdist bdist_wheel @@ -55,7 +45,7 @@ Steps:: Check install capability for the wheel -------------------------------------- -Steps:: +Steps: virtualenv .test_venv -ppython3.6 source .test_venv/bin/activate @@ -65,14 +55,14 @@ Steps:: Submit release to PYPI ---------------------- -Steps:: +Steps: twine upload dist/fdk-X.X.X* Verify install capability for the wheel --------------------------------------- -Steps:: +Steps: virtualenv .new_venv -ppython3.6 source .new_venv/bin/activate diff --git a/requirements.txt b/requirements.txt index 09305cb..ee97c53 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0 ujson==1.35 requests==2.18.4 dill==0.2.7.1 +iso8601==0.1.12 diff --git a/samples/hot/http/async-echo/func.py b/samples/hot/http/async-echo/func.py index f7f9831..e4887e9 100644 --- a/samples/hot/http/async-echo/func.py +++ b/samples/hot/http/async-echo/func.py @@ -15,15 +15,15 @@ import asyncio import fdk -from fdk.http import response +from fdk import response -@fdk.coerce_http_input_to_content_type +@fdk.coerce_input_to_content_type async def app(context, data=None, loop=None): """ This is just an echo function :param context: request context - :type context: hotfn.http.request.RequestContext + :type context: fdk.context.RequestContext :param data: request body :type data: object :param loop: asyncio event loop @@ -35,7 +35,7 @@ async def app(context, data=None, loop=None): "Content-Type": "plain/text", } return response.RawResponse( - http_proto_version=context.version, + context, status_code=200, headers=headers, response_data="OK") diff --git a/samples/hot/http/async-echo/func.yaml b/samples/hot/http/async-echo/func.yaml index 65d6a54..4576a07 100644 --- a/samples/hot/http/async-echo/func.yaml +++ b/samples/hot/http/async-echo/func.yaml @@ -1,6 +1,6 @@ -name: test/hotfn +name: fnproject/fdk-python-async-echo:0.0.1 version: 0.0.1 runtime: python3 format: http timeout: 100 -path: /hotfnpy-hot +path: /async-echo diff --git a/samples/hot/http/async-echo/requirements.txt b/samples/hot/http/async-echo/requirements.txt index 58e0113..ec531cf 100644 --- a/samples/hot/http/async-echo/requirements.txt +++ b/samples/hot/http/async-echo/requirements.txt @@ -1 +1 @@ -fdk==0.0.1 +fdk==0.0.5 diff --git a/samples/hot/http/response-writer/Dockerfile b/samples/hot/http/custom-response/Dockerfile similarity index 100% rename from samples/hot/http/response-writer/Dockerfile rename to samples/hot/http/custom-response/Dockerfile diff --git a/samples/hot/http/response-writer/func.py b/samples/hot/http/custom-response/func.py similarity index 91% rename from samples/hot/http/response-writer/func.py rename to samples/hot/http/custom-response/func.py index 2410166..1f12fe7 100644 --- a/samples/hot/http/response-writer/func.py +++ b/samples/hot/http/custom-response/func.py @@ -13,10 +13,11 @@ # under the License. import fdk -from fdk.http import response +from fdk import response -@fdk.coerce_http_input_to_content_type + +@fdk.coerce_input_to_content_type def app(context, data=None, loop=None): """ This is just an echo function @@ -33,7 +34,7 @@ def app(context, data=None, loop=None): "Content-Type": "plain/text", } rs = response.RawResponse( - http_proto_version=context.version, + context, status_code=200, headers=headers, response_data="OK") diff --git a/samples/hot/http/custom-response/func.yaml b/samples/hot/http/custom-response/func.yaml new file mode 100644 index 0000000..3ed3114 --- /dev/null +++ b/samples/hot/http/custom-response/func.yaml @@ -0,0 +1,6 @@ +name: fnproject/fdk-python-custom-response:0.0.1 +version: 0.0.1 +runtime: python3 +format: http +timeout: 100 +path: /custom_response diff --git a/samples/hot/http/custom-response/requirements.txt b/samples/hot/http/custom-response/requirements.txt new file mode 100644 index 0000000..ec531cf --- /dev/null +++ b/samples/hot/http/custom-response/requirements.txt @@ -0,0 +1 @@ +fdk==0.0.5 diff --git a/samples/hot/http/echo/func.py b/samples/hot/http/echo/func.py index e274cfe..c90d08b 100644 --- a/samples/hot/http/echo/func.py +++ b/samples/hot/http/echo/func.py @@ -15,7 +15,7 @@ import fdk -@fdk.coerce_http_input_to_content_type +@fdk.coerce_input_to_content_type def app(context, data=None, loop=None): """ This is just an echo function diff --git a/samples/hot/http/echo/func.yaml b/samples/hot/http/echo/func.yaml index 65d6a54..af8f902 100644 --- a/samples/hot/http/echo/func.yaml +++ b/samples/hot/http/echo/func.yaml @@ -1,6 +1,6 @@ -name: test/hotfn +name: fnproject/fdk-python-echo:0.0.1 version: 0.0.1 runtime: python3 format: http timeout: 100 -path: /hotfnpy-hot +path: /echo diff --git a/samples/hot/http/echo/requirements.txt b/samples/hot/http/echo/requirements.txt index 58e0113..ec531cf 100644 --- a/samples/hot/http/echo/requirements.txt +++ b/samples/hot/http/echo/requirements.txt @@ -1 +1 @@ -fdk==0.0.1 +fdk==0.0.5 diff --git a/samples/hot/http/http_requests.go b/samples/hot/http/http_requests.go deleted file mode 100644 index 5a51c61..0000000 --- a/samples/hot/http/http_requests.go +++ /dev/null @@ -1,56 +0,0 @@ -package main - -import ( - "fmt" - "net/http" - "bytes" - "net/url" - "io/ioutil" - "net/http/httputil" - "strconv" - "time" - "os" -) - -func createRequest(data string) { - var buf bytes.Buffer - - req := http.Request{ - Method: http.MethodGet, - URL: &url.URL{ - Scheme: "http", - Host: "localhost:8080", - Path: "/v1/apps", - RawQuery: "something=something&etc=etc", - }, - ProtoMajor: 1, - ProtoMinor: 1, - Header: http.Header{ - "Host": []string{"localhost:8080"}, - "User-Agent": []string{"curl/7.51.0"}, - "Content-Length": []string{strconv.Itoa(len(data))}, - "Content-Type": []string{"application/text"}, - }, - ContentLength: int64(len(data)), - Host: "localhost:8080", - } - buf.Write([]byte(data)) - req.Body = ioutil.NopCloser(&buf) - raw, err := httputil.DumpRequest(&req, true) - if err != nil { - fmt.Println(fmt.Sprintf("Error %v", err)) - } - fmt.Println(string(raw)) -} - - -func main() { - hot := os.Getenv("HOT") - createRequest("hello:hello") - if hot != "" { - time.Sleep(2 * time.Second) - createRequest("secondtest") - time.Sleep(4 * time.Second) - createRequest(`{"name": "John"}`) - } -} diff --git a/samples/hot/http/response-writer/func.yaml b/samples/hot/http/response-writer/func.yaml deleted file mode 100644 index 65d6a54..0000000 --- a/samples/hot/http/response-writer/func.yaml +++ /dev/null @@ -1,6 +0,0 @@ -name: test/hotfn -version: 0.0.1 -runtime: python3 -format: http -timeout: 100 -path: /hotfnpy-hot diff --git a/samples/hot/http/response-writer/requirements.txt b/samples/hot/http/response-writer/requirements.txt deleted file mode 100644 index 58e0113..0000000 --- a/samples/hot/http/response-writer/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -fdk==0.0.1 diff --git a/samples/hot/json/echo/func.yaml b/samples/hot/json/echo/func.yaml index f3826a4..a34ca66 100644 --- a/samples/hot/json/echo/func.yaml +++ b/samples/hot/json/echo/func.yaml @@ -1,4 +1,4 @@ -name: denismakogon/hot-json-python:0.0.1 +name: fnproject/hot-json-python:0.0.1 version: 0.0.1 runtime: docker type: sync diff --git a/samples/hot/json/echo/requirements.txt b/samples/hot/json/echo/requirements.txt index 58e0113..ec531cf 100644 --- a/samples/hot/json/echo/requirements.txt +++ b/samples/hot/json/echo/requirements.txt @@ -1 +1 @@ -fdk==0.0.1 +fdk==0.0.5 diff --git a/samples/python3-fn-gpi/func.py b/samples/python3-fn-gpi/func.py index 4ef11b8..ca8177b 100644 --- a/samples/python3-fn-gpi/func.py +++ b/samples/python3-fn-gpi/func.py @@ -15,12 +15,12 @@ import asyncio import dill import fdk -import json import sys -from fdk.http import response +from fdk import response +@fdk.coerce_input_to_content_type async def handler(context, data=None, loop=None): """ General purpose Python3 function processor @@ -41,20 +41,19 @@ async def handler(context, data=None, loop=None): :param context: request context :type context: fdk.context.RequestContext :param data: request data - :type data: fdk.http.request.ContentLengthStream + :type data: dict :param loop: asyncio event loop :type loop: asyncio.AbstractEventLoop :return: resulting object of distributed function :rtype: object """ - action_dict = json.loads(data.read()) (is_coroutine, self_in_bytes, action_in_bytes, action_args, action_kwargs) = ( - action_dict['is_coroutine'], - action_dict['self'], - action_dict['action'], - action_dict['args'], - action_dict['kwargs']) + data['is_coroutine'], + data['self'], + data['action'], + data['args'], + data['kwargs']) print("Got {} bytes of class instance".format(len(self_in_bytes)), file=sys.stderr, flush=True) @@ -89,7 +88,7 @@ async def handler(context, data=None, loop=None): except Exception as ex: print("call failed", file=sys.stderr, flush=True) return response.RawResponse( - http_proto_version=context.version, + context, status_code=500, headers={ "Content-Type": "text/plain", @@ -102,7 +101,7 @@ async def handler(context, data=None, loop=None): res = await res return response.RawResponse( - http_proto_version=context.version, + context, status_code=200, headers={ "Content-Type": "text/plain", diff --git a/samples/python3-fn-gpi/func.yaml b/samples/python3-fn-gpi/func.yaml index 72a787d..d58c6cb 100644 --- a/samples/python3-fn-gpi/func.yaml +++ b/samples/python3-fn-gpi/func.yaml @@ -1,4 +1,4 @@ -name: denismakogon/python3-fn-gpi:0.0.1 +name: fnproject/python3-fn-gpi:0.0.1 version: 0.0.1 runtime: docker type: sync diff --git a/samples/python3-fn-gpi/requirements.txt b/samples/python3-fn-gpi/requirements.txt index 79eaa80..07bffd7 100644 --- a/samples/python3-fn-gpi/requirements.txt +++ b/samples/python3-fn-gpi/requirements.txt @@ -1,3 +1,2 @@ -fdk==0.0.2 -cloudpickle==0.4.0 +fdk==0.0.5 dill==0.2.7.1 diff --git a/setup.cfg b/setup.cfg index 62656b7..cfe18f6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,4 @@ [metadata] -version=0.0.4 name = fdk summary = Function Developer Kit for Python description-file = diff --git a/tox.ini b/tox.ini index 98bf4e7..ee2d01b 100644 --- a/tox.ini +++ b/tox.ini @@ -33,6 +33,6 @@ commands = pytest -v -s --tb=long --cov=fdk {toxinidir}/fdk/tests commands = pytest -v -s --tb=long --cov=fdk {toxinidir}/fdk/tests [flake8] -ignore = H405,H404,H403,H401 +ignore = H405,H404,H403,H401,H306 show-source = True exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,docs,venv,.venv