Skip to content

Commit d06f3dd

Browse files
authored
FDK Context| deadline | generic response, etc. (#16)
* FDK context * Generic response object * Support for FN_DEADLINE * Fixing status codes problem * Custom response object tests * Test functions that raising exceptions - use safe way to recover from exceptions for both JSON/HTTP handlers - confirm that status codes are set properly for edge cases like deadline * Cleaning up before release * Data coercing tests * Cleanin up comments * Updating samples
1 parent 63e6ad9 commit d06f3dd

Some content is hidden

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

44 files changed

+699
-403
lines changed

Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ FROM python:3.6.2
33

44
RUN mkdir /code
55
ADD . /code/
6-
RUN pip install -e /code/
6+
RUN pip3 install -r /code/requirements.txt
7+
RUN pip3 install -e /code/
78

89
WORKDIR /code/fdk/tests/fn/traceback
910
ENTRYPOINT ["python3", "func.py"]

README.md

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@ In order to utilise this, you can write your `app.py` as follows:
1414
```python
1515
import fdk
1616

17-
from fdk.http import response
17+
from fdk import response
1818

1919

2020
def handler(context, data=None, loop=None):
2121
return response.RawResponse(
22-
http_proto_version=context.version,
23-
status_code=200,
24-
headers={},
22+
context,
23+
status_code=200,
24+
headers={},
2525
response_data=data.readall()
2626
)
2727

@@ -31,7 +31,7 @@ if __name__ == "__main__":
3131

3232
```
3333

34-
Automatic HTTP input coercions
34+
Automatic input coercions
3535
------------------------------
3636

3737
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:
4040
```python
4141
import fdk
4242

43-
@fdk.coerce_http_input_to_content_type
43+
44+
@fdk.coerce_input_to_content_type
4445
def handler(context, data=None, loop=None):
4546
"""
4647
body is a request body, it's type depends on content type
@@ -53,24 +54,24 @@ if __name__ == "__main__":
5354

5455
```
5556

56-
Working with async automatic HTTP input coercions
57+
Working with async automatic input coercions
5758
-------------------------------------------------
5859

5960
Latest version supports async coroutines as a request body processors:
6061
```python
6162
import asyncio
6263
import fdk
6364

64-
from fdk.http import response
65+
from fdk import response
6566

6667

67-
@fdk.coerce_http_input_to_content_type
68+
@fdk.coerce_input_to_content_type
6869
async def handler(context, data=None, loop=None):
6970
headers = {
7071
"Content-Type": "text/plain",
7172
}
7273
return response.RawResponse(
73-
http_proto_version=context.version,
74+
context,
7475
status_code=200,
7576
headers=headers,
7677
response_data="OK"
@@ -82,7 +83,7 @@ if __name__ == "__main__":
8283
fdk.handle(handler, loop=loop)
8384

8485
```
85-
As you can see `app` function is no longer callable, because its type: coroutine, so we need to bypass event loop inside
86+
As you can see `app` function is no longer callable, because its type: coroutine, so we need to bypass event loop inside
8687

8788
Handling Hot JSON Functions
8889
---------------------------
@@ -126,7 +127,7 @@ if __name__ == "__main__":
126127
Applications powered by Fn: Concept
127128
-----------------------------------
128129

129-
FDK is not only about developing functions, but providing necessary API to build serverless applications
130+
FDK is not only about developing functions, but providing necessary API to build serverless applications
130131
that look like nothing but classes with methods powered by Fn.
131132

132133
```python
@@ -163,6 +164,7 @@ class Application(object):
163164
r.raise_for_status()
164165
return r.text
165166

167+
166168
if __name__ == "__main__":
167169
app = Application(config={})
168170

@@ -190,15 +192,15 @@ if __name__ == "__main__":
190192
In order to identify to which Fn instance code needs to talk set following env var:
191193

192194
```bash
193-
export API_URL=http://localhost:8080
195+
export API_URL = http: // localhost: 8080
194196
```
195197
with respect to IP address or domain name where Fn lives.
196198

197199

198200
Applications powered by Fn: supply data to a function
199201
-----------------------------------------------------
200202

201-
At this moment those helper-decorators let developers interact with Fn-powered functions as with regular class methods.
203+
At this moment those helper - decorators let developers interact with Fn - powered functions as with regular class methods.
202204
In order to pass necessary data into a function developer just needs to do following
203205
```python
204206

@@ -208,13 +210,13 @@ if __name__ == "__main__":
208210
app.env(keyone="blah", keytwo="blah", somethingelse=3)
209211

210212
```
211-
Key-value args will be turned into JSON instance and will be sent to a function as payload body.
213+
Key - value args will be turned into JSON instance and will be sent to a function as payload body.
212214

213215

214216
Applications powered by Fn: working with function's result
215217
----------------------------------------------------------
216218

217-
In order to work with result from function you just need to read key-value argument `fn_data`:
219+
In order to work with result from function you just need to read key - value argument `fn_data`:
218220
```python
219221
@decorators.with_fn(fn_image="denismakogon/py-traceback-test:0.0.1",
220222
fn_format="http")
@@ -225,7 +227,7 @@ In order to work with result from function you just need to read key-value argum
225227
Applications powered by Fn: advanced serverless functions
226228
---------------------------------------------------------
227229

228-
Since release v0.0.3 developer can consume new API to build truly serverless functions
230+
Since release v0.0.3 developer can consume new API to build truly serverless functions
229231
without taking care of Docker images, application, etc.
230232

231233
```python
@@ -247,29 +249,29 @@ Each function decorated with `@decorator.fn` will become truly serverless and di
247249
So, how it works?
248250

249251
* A developer writes function
250-
* FDK (Fn-powered app) creates a recursive Pickle v4.0 with 3rd-party dependencies
251-
* FDK (Fn-powered app) transfers pickled object to a function based on Python3 GPI (general purpose image)
252-
* FDK unpickles function and its 3rd-party dependencies and runs it
253-
* Function sends response back to Fn-powered application function caller
252+
* FDK(Fn - powered app) creates a recursive Pickle v4.0 with 3rd - party dependencies
253+
* FDK(Fn - powered app) transfers pickled object to a function based on Python3 GPI(general purpose image)
254+
* FDK unpickles function and its 3rd - party dependencies and runs it
255+
* Function sends response back to Fn - powered application function caller
254256

255-
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).
257+
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).
256258

257259

258260
Applications powered by Fn: exceptions
259261
--------------------------------------
260262

261-
Applications powered by Fn are following Go-like errors concept. It gives you full control on errors whether raise them or not.
263+
Applications powered by Fn are following Go - like errors concept. It gives you full control on errors whether raise them or not.
262264
```python
263265
res, err = app.env()
264266
if err:
265267
raise err
266268
print(res)
267269

268270
```
269-
Each error is an instance fn `FnError` that encapsulates certain logic that makes hides HTTP errors and turns them into regular Python-like exceptions.
271+
Each error is an instance fn `FnError` that encapsulates certain logic that makes hides HTTP errors and turns them into regular Python - like exceptions.
270272

271273
TODOs
272274
-----
273275

274-
- generic response class
275-
- use fdk.headers.GoLikeHeaders in http
276+
- generic response class
277+
- use fdk.headers.GoLikeHeaders in http

fdk/__init__.py

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,57 @@
1212
# License for the specific language governing permissions and limitations
1313
# under the License.
1414

15-
from fdk.http import handle as http_handler
16-
from fdk import runner
15+
import functools
16+
import io
17+
import ujson
1718

19+
from fdk import runner
1820

19-
coerce_http_input_to_content_type = http_handler.coerce_input_to_content_type
2021
handle = runner.generic_handle
2122

23+
24+
def coerce_input_to_content_type(request_data_processor):
25+
26+
@functools.wraps(request_data_processor)
27+
def app(context, data=None, loop=None):
28+
"""
29+
Request handler app dispatcher decorator
30+
:param context: request context
31+
:type context: request.RequestContext
32+
:param data: request body
33+
:type data: io.BufferedIOBase
34+
:param loop: asyncio event loop
35+
:type loop: asyncio.AbstractEventLoop
36+
:return: raw response
37+
:rtype: response.RawResponse
38+
:return:
39+
"""
40+
body = data
41+
content_type = context.Headers().get("content-type")
42+
try:
43+
44+
if hasattr(data, "readable"):
45+
request_body = io.TextIOWrapper(data)
46+
else:
47+
request_body = data
48+
49+
if content_type == "application/json":
50+
if isinstance(request_body, str):
51+
body = ujson.loads(request_body)
52+
else:
53+
body = ujson.load(request_body)
54+
elif content_type in ["text/plain"]:
55+
body = request_body.read()
56+
57+
except Exception as ex:
58+
raise context.DispatchError(
59+
context, 500, "Unexpected error: {}".format(str(ex)))
60+
61+
return request_data_processor(context, data=body, loop=loop)
62+
63+
return app
64+
65+
2266
__all__ = [
23-
'coerce_http_input_to_content_type',
2467
'handle'
2568
]

fdk/context.py

Lines changed: 68 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,29 +12,78 @@
1212
# License for the specific language governing permissions and limitations
1313
# under the License.
1414

15+
from fdk import errors
16+
1517

1618
class RequestContext(object):
1719

18-
def __init__(self, method=None, url=None,
19-
query_parameters=None, headers=None,
20-
version=None):
20+
def __init__(self, app_name, route, call_id,
21+
fntype, config=None, headers=None, arguments=None):
2122
"""
2223
Request context here to be a placeholder
2324
for request-specific attributes
24-
:param method: HTTP request method
25-
:type method: str
26-
:param url: HTTP request URL
27-
:type url: str
28-
:param query_parameters: HTTP request query parameters
29-
:type query_parameters: dict
30-
:param headers: HTTP request headers
31-
:type headers: object
32-
:param version: HTTP proto version
33-
:type version: tuple
3425
"""
35-
# TODO(xxx): app name, path, memory, type, config
36-
self.method = method
37-
self.url = url
38-
self.query_parameters = query_parameters
39-
self.headers = headers
40-
self.version = version
26+
self.__app_name = app_name
27+
self.__app_route = route
28+
self.__call_id = call_id
29+
self.__config = config if config else {}
30+
self.__headers = headers if headers else {}
31+
self.__arguments = {} if not arguments else arguments
32+
self.__type = fntype
33+
34+
def AppName(self):
35+
return self.__app_name
36+
37+
def Route(self):
38+
return self.__app_route
39+
40+
def CallID(self):
41+
return self.__call_id
42+
43+
def Config(self):
44+
return self.__config
45+
46+
def Headers(self):
47+
return self.__headers
48+
49+
def Arguments(self):
50+
return self.__arguments
51+
52+
def Type(self):
53+
return self.__type
54+
55+
56+
class HTTPContext(RequestContext):
57+
58+
def __init__(self, app_name, route,
59+
call_id, fntype="http",
60+
config=None, headers=None,
61+
method=None, url=None,
62+
query_parameters=None,
63+
version=None):
64+
arguments = {
65+
"method": method,
66+
"URL": url,
67+
"query": query_parameters,
68+
"http_version": version
69+
}
70+
self.DispatchError = errors.HTTPDispatchException
71+
super(HTTPContext, self).__init__(
72+
app_name, route, call_id, fntype,
73+
config=config, headers=headers, arguments=arguments)
74+
75+
76+
class JSONContext(RequestContext):
77+
78+
def __init__(self, app_name, route, call_id,
79+
fntype="json", config=None, headers=None):
80+
self.DispatchError = errors.JSONDispatchException
81+
super(JSONContext, self).__init__(
82+
app_name, route, call_id, fntype, config=config, headers=headers)
83+
84+
85+
def fromType(fntype, *args, **kwargs):
86+
if fntype == "json":
87+
return JSONContext(*args, **kwargs)
88+
if fntype == "http":
89+
return HTTPContext(*args, **kwargs)

0 commit comments

Comments
 (0)