Skip to content

Commit

Permalink
Release v0.2.0
Browse files Browse the repository at this point in the history
* add cors and test with new azure functions lib

* bump version
  • Loading branch information
eduardomourar authored May 28, 2019
1 parent 9e79de8 commit 078dcf7
Show file tree
Hide file tree
Showing 7 changed files with 237 additions and 150 deletions.
6 changes: 3 additions & 3 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
},
"debug.internalConsoleOptions": "neverOpen",
"python.linting.enabled": true,
"python.unitTest.unittestEnabled": false,
"python.unitTest.nosetestsEnabled": false,
"python.unitTest.pyTestEnabled": true
"python.testing.unittestEnabled": false,
"python.testing.nosetestsEnabled": false,
"python.testing.pyTestEnabled": true
}
3 changes: 3 additions & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ functionapprest = {editable = true,path = "."}

[requires]
python_version = "3.6"

[pipenv]
allow_prereleases = true
209 changes: 115 additions & 94 deletions Pipfile.lock

Large diffs are not rendered by default.

96 changes: 52 additions & 44 deletions functionapprest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@
from werkzeug.routing import Map, Rule, NotFound
from werkzeug.urls import url_parse
from azure.functions import HttpRequest, HttpResponse, Context
from azure.functions._http import HttpResponseHeaders


__validate_kwargs = {'format_checker': FormatChecker()}
__required_keys = ['method', 'url']
__default_headers = {
'Access-Control-Allow-Headers': '*',
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json'
}

Expand Down Expand Up @@ -198,40 +197,6 @@ def set_body(self, body):
self.__body_bytes = bytes(body)


class Response(HttpResponse):
"""Class to conceptualize a response with default attributes
if no body is specified, empty string is returned
if no status_code is specified, 200 is returned
if no headers are specified, empty dict is returned
"""

def __init__(self, body=None, status_code=None, headers=None, *,
mimetype='application/json', charset='utf-8'):
self.json = None
if isinstance(body, (dict, list)):
self.json = body
body = json.dumps(body, default=_json_serial)
super(Response, self).__init__(body, status_code=status_code, headers=headers, mimetype=mimetype, charset=charset)

def get_body_string(self) -> str:
"""Response body as a string."""

body = self.json
if body is None:
body_bytes = self.get_body() or b''
body = body_bytes.decode(self.charset)
if body:
return json.dumps(body, default=_json_serial)
return ''

def to_json(self):
return {
'body': self.get_body_string(),
'status_code': self.status_code or 200,
'headers': self.headers or {}
}


def _float_cast(value):
try:
return float(value)
Expand Down Expand Up @@ -283,11 +248,10 @@ def _options_response(req: Request, methods: list):
body = {
'allow': allowed_methods
}
headers = __default_headers
headers.update({
headers = {
'Access-Control-Allow-Methods': allowed_methods
})
return Response(body, 200, headers)
}
return (body, 200, headers)


def default_error_handler(error, method: str):
Expand All @@ -302,7 +266,7 @@ def default_error_handler(error, method: str):
}, 500)


def create_functionapp_handler(error_handler=default_error_handler):
def create_functionapp_handler(error_handler=default_error_handler, headers=None):
"""Create a functionapp handler function with `handle` decorator as attribute
example:
Expand All @@ -324,6 +288,50 @@ def my_get_func(req):
JSON schema, please see http://json-schema.org for info.
"""
url_maps = Map()
if headers is None:
headers = __default_headers
default_headers = HttpResponseHeaders(headers)
if os.environ.get('AZURE_FUNCTIONS_ENVIRONMENT', '').lower() in ('dev', 'development'):
default_headers.update({
'Access-Control-Allow-Headers': '*',
'Access-Control-Allow-Origin': '*'
})

class Response(HttpResponse):
"""Class to conceptualize a response with default attributes
if no body is specified, empty string is returned
if no status_code is specified, 200 is returned
if no headers are specified, empty dict is returned
"""

def __init__(self, body=None, status_code=None, headers=None, *,
mimetype='application/json', charset='utf-8'):
self.json = None
if isinstance(body, (dict, list)):
self.json = body
body = json.dumps(body, default=_json_serial)
original_headers = headers or {}
headers = default_headers
headers.update(original_headers)
super(Response, self).__init__(body, status_code=status_code, headers=headers, mimetype=mimetype, charset=charset)

def get_body_string(self) -> str:
"""Response body as a string."""

body = self.json
if body is None:
body_bytes = self.get_body() or b''
body = body_bytes.decode(self.charset)
if body:
return json.dumps(body, default=_json_serial)
return ''

def to_json(self):
return {
'body': self.get_body_string(),
'status_code': self.status_code or 200,
'headers': dict(self.headers or {})
}

def inner_functionapp_handler(req: Request, context: FunctionsContext):
# check if running as Azure Functions
Expand Down Expand Up @@ -365,7 +373,8 @@ def inner_functionapp_handler(req: Request, context: FunctionsContext):
# bind the mapping to an empty server name
mapping = url_maps.bind('')
if method_name == 'options':
return _options_response(req, mapping.allowed_methods(path))
body, status_code, headers = _options_response(req, mapping.allowed_methods(path))
return Response(body, status_code, headers)
rule, kwargs = mapping.match(path, method=method_name, return_rule=True)
func = rule.endpoint

Expand Down Expand Up @@ -425,8 +434,7 @@ def inner_functionapp_handler(req: Request, context: FunctionsContext):

def inner_handler(method_name, path='/', schema=None, load_json=True):
if schema and not load_json:
raise ValueError(
'if schema is supplied, load_json needs to be true')
raise ValueError('if schema is supplied, load_json needs to be true')

def wrapper(func):
@functools.wraps(func)
Expand Down
2 changes: 1 addition & 1 deletion functionapprest/__version__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
__author__ = 'eduardomourar'
__email__ = 'eduardo_demoura@yahoo.com.br'
__version__ = '0.1.2'
__version__ = '0.2.0'
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
requirements = [
'jsonschema>=2.5.1',
'strict_rfc3339>=0.7',
'azure-functions==1.0.0b3',
'azure-functions>=1.0.0b3',
'werkzeug>=0.14.1',
]

Expand Down
69 changes: 62 additions & 7 deletions tests/test_functionapprest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
except ImportError:
import mock

import os
import unittest
import json
import copy
import random
from datetime import datetime
import time

from datetime import datetime

from functionapprest import create_functionapp_handler, Request, FunctionsContext


Expand All @@ -21,7 +23,7 @@ def assert_called_once(mock):
assert mock.call_count == 1


class TestfunctionapprestFunctions(unittest.TestCase):
class TestFunctions(unittest.TestCase):
def setUp(self):
self.event = Request('POST', 'http://localhost:7071/api/v1/')
self.context = FunctionsContext(
Expand All @@ -30,7 +32,9 @@ def setUp(self):
invocation_id='c9b749e6-0611-4b651-9ff0-cdd2da18f05b',
bindings={}
)
self.functionapp_handler = create_functionapp_handler()
self.env = mock.patch.dict('os.environ', {'AZURE_FUNCTIONS_ENVIRONMENT': 'production'})
with self.env:
self.functionapp_handler = create_functionapp_handler(headers={})

def test_post_validation_success(self):
json_body = dict(
Expand Down Expand Up @@ -343,7 +347,8 @@ def test_exception_in_handler_should_be_reraised(self):
def divide_by_zero(_):
return 1/0

self.functionapp_handler = create_functionapp_handler(error_handler=None)
with self.env:
self.functionapp_handler = create_functionapp_handler(error_handler=None, headers={})
self.functionapp_handler.handle('get', path='/foo/bar')(divide_by_zero)

with self.assertRaises(ZeroDivisionError):
Expand All @@ -354,14 +359,64 @@ def test_routing_with_multiple_decorators(self):

self.event.set_body(json.dumps(json_body))
self.event.method = 'GET'
headers = {
'content-type': 'application/json',
'access-control-allow-origin': '*'
}

self.functionapp_handler = create_functionapp_handler(error_handler=None)
with self.env:
self.functionapp_handler = create_functionapp_handler(error_handler=None, headers={})

def test_routing(event, id):
return {'my-id': id}
return ({'my-id': id}, 200, headers)

self.functionapp_handler.handle('get', path='/foo/<int:id>/')(test_routing)
self.functionapp_handler.handle('options', path='/foo/<int:id>/')(test_routing)
self.event.url = '/foo/1234/'
result = self.functionapp_handler(self.event, self.context).to_json()
assert result == {'body': '{"my-id": 1234}', 'status_code': 200, 'headers':{}}
assert result == {'body': '{"my-id": 1234}', 'status_code': 200, 'headers': headers}

def test_default_header_in_development(self):
json_body = {}

self.event.set_body(json.dumps(json_body))
self.event.method = 'GET'
headers = {
'access-control-allow-headers': '*',
'content-type': 'application/json',
'access-control-allow-origin': '*'
}

with mock.patch.dict('os.environ', {'AZURE_FUNCTIONS_ENVIRONMENT': 'development'}):
environ = os.environ.get('AZURE_FUNCTIONS_ENVIRONMENT', '')
self.functionapp_handler = create_functionapp_handler(error_handler=None, headers=headers)

def test_env_development(event):
return environ

self.functionapp_handler.handle('get', path='/foo/')(test_env_development)
self.event.url = '/foo/'
result = self.functionapp_handler(self.event, self.context).to_json()
assert result == {'body': '"development"', 'status_code': 200, 'headers': headers}

def test_default_header_in_production(self):
json_body = {}

self.event.set_body(json.dumps(json_body))
self.event.method = 'GET'
headers = {
'access-control-allow-headers': '*',
'content-type': 'application/json'
}

with mock.patch.dict('os.environ', {'AZURE_FUNCTIONS_ENVIRONMENT': 'production'}):
environ = os.environ.get('AZURE_FUNCTIONS_ENVIRONMENT', '')
self.functionapp_handler = create_functionapp_handler(error_handler=None, headers=headers)

def test_env_production(event):
return environ

self.functionapp_handler.handle('get', path='/bar/')(test_env_production)
self.event.url = '/bar/'
result = self.functionapp_handler(self.event, self.context).to_json()
assert result == {'body': '"production"', 'status_code': 200, 'headers': headers}

0 comments on commit 078dcf7

Please sign in to comment.