Skip to content

Commit 1851e07

Browse files
authored
Merge pull request #27 from MagicStack/httpr
Implement HttpResponse support (see issue #4)
2 parents fdf5fbd + d2372dc commit 1851e07

File tree

12 files changed

+309
-39
lines changed

12 files changed

+309
-39
lines changed

azure/functions/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
from .abc import Context, Out, HttpRequest, HttpResponse # NoQA
1+
from ._abc import Context, Out # NoQA
2+
from ._http import HttpRequest, HttpResponse # NoQA

azure/functions/abc.py renamed to azure/functions/_abc.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,5 +76,6 @@ def status_code(self):
7676
def headers(self):
7777
pass
7878

79-
def set_body(self, body: TypedData):
79+
@abc.abstractmethod
80+
def get_body(self) -> bytes:
8081
pass

azure/functions/_http.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import collections.abc
2+
import types
3+
import typing
4+
5+
from . import _abc
6+
7+
8+
class BaseHeaders(collections.abc.Mapping):
9+
10+
def __init__(self, source=None):
11+
if source is None:
12+
self.__http_headers__ = {}
13+
else:
14+
self.__http_headers__ = {k.lower(): v for k, v in source.items()}
15+
16+
def __getitem__(self, key: str) -> str:
17+
return self.__http_headers__[key.lower()]
18+
19+
def __len__(self):
20+
return len(self.__http_headers__)
21+
22+
def __contains__(self, key: str):
23+
return key.lower() in self.__http_headers__
24+
25+
def __iter__(self):
26+
return iter(self.__http_headers__)
27+
28+
29+
class RequestHeaders(BaseHeaders):
30+
pass
31+
32+
33+
class ResponseHeaders(BaseHeaders, collections.abc.MutableMapping):
34+
35+
def __setitem__(self, key: str, value: str):
36+
self.__http_headers__[key.lower()] = value
37+
38+
def __delitem__(self, key: str):
39+
del self.__http_headers__[key.lower()]
40+
41+
42+
class HttpRequest(_abc.HttpRequest):
43+
"""An HTTP request object."""
44+
45+
def __init__(self, method: str, url: str,
46+
headers: typing.Mapping[str, str],
47+
params: typing.Mapping[str, str],
48+
body):
49+
self.__method = method
50+
self.__url = url
51+
self.__headers = RequestHeaders(headers)
52+
self.__params = types.MappingProxyType(params)
53+
self.__body = body
54+
55+
@property
56+
def url(self):
57+
return self.__url
58+
59+
@property
60+
def method(self):
61+
return self.__method.upper()
62+
63+
@property
64+
def headers(self):
65+
return self.__headers
66+
67+
@property
68+
def params(self):
69+
return self.__params
70+
71+
def get_body(self):
72+
return self.__body
73+
74+
75+
class HttpResponse(_abc.HttpResponse):
76+
"""An HTTP response object."""
77+
78+
def __init__(self, body=None, *,
79+
status_code=None, headers=None, mimetype=None, charset=None):
80+
if status_code is None:
81+
status_code = 200
82+
self.__status_code = status_code
83+
84+
if mimetype is None:
85+
mimetype = 'text/plain'
86+
self.__mimetype = mimetype
87+
88+
if charset is None:
89+
charset = 'utf-8'
90+
self.__charset = charset
91+
92+
if headers is None:
93+
headers = {}
94+
self.__headers = ResponseHeaders(headers)
95+
96+
if body is not None:
97+
self.__set_body(body)
98+
else:
99+
self.__body = None
100+
101+
@property
102+
def mimetype(self):
103+
return self.__mimetype
104+
105+
@property
106+
def charset(self):
107+
return self.__charset
108+
109+
@property
110+
def headers(self):
111+
return self.__headers
112+
113+
@property
114+
def status_code(self):
115+
return self.__status_code
116+
117+
def __set_body(self, body):
118+
if isinstance(body, str):
119+
body = body.encode(self.__charset)
120+
121+
if not isinstance(body, (bytes, bytearray)):
122+
raise TypeError(
123+
f'reponse is expected to be either of '
124+
f'str, bytes, or bytearray, got {type(body).__name__}')
125+
126+
self.__body = body
127+
128+
def get_body(self):
129+
return self.__body

azure/worker/rpc_types.py

Lines changed: 23 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
"""
66

77

8-
import types
98
import typing
109

1110
import azure.functions as azf
@@ -44,41 +43,6 @@ def function_directory(self) -> str:
4443
return self.__func_dir
4544

4645

47-
class HttpRequest(azf.HttpRequest):
48-
"""An HTTP request object."""
49-
50-
__slots__ = ('_method', '_url', '_headers', '_params', '_body')
51-
52-
def __init__(self, method: str, url: str,
53-
headers: typing.Mapping[str, str],
54-
params: typing.Mapping[str, str],
55-
body):
56-
self._method = method
57-
self._url = url
58-
self._headers = types.MappingProxyType(headers)
59-
self._params = types.MappingProxyType(params)
60-
self._body = body
61-
62-
@property
63-
def url(self):
64-
return self._url
65-
66-
@property
67-
def method(self):
68-
return self._method.upper()
69-
70-
@property
71-
def headers(self):
72-
return self._headers
73-
74-
@property
75-
def params(self):
76-
return self._params
77-
78-
def get_body(self):
79-
return self._body
80-
81-
8246
def from_incoming_proto(o: protos.TypedData):
8347
dt = o.WhichOneof('data')
8448

@@ -89,7 +53,7 @@ def from_incoming_proto(o: protos.TypedData):
8953
return getattr(o, dt)
9054

9155
if dt == 'http':
92-
return HttpRequest(
56+
return azf.HttpRequest(
9357
method=o.http.method,
9458
url=o.http.url,
9559
headers=o.http.headers,
@@ -114,6 +78,28 @@ def to_outgoing_proto(o: typing.Any):
11478
if isinstance(o, bytes):
11579
return protos.TypedData(bytes=o)
11680

81+
if isinstance(o, azf.HttpResponse):
82+
status = o.status_code
83+
headers = dict(o.headers)
84+
if 'content-type' not in headers:
85+
if o.mimetype.startswith('text/'):
86+
headers['content-type'] = f'{o.mimetype}; charset={o.charset}'
87+
else:
88+
headers['content-type'] = f'{o.mimetype}'
89+
90+
body = o.get_body()
91+
if body is not None:
92+
body = protos.TypedData(bytes=body)
93+
else:
94+
body = protos.TypedData(bytes=b'')
95+
96+
return protos.TypedData(
97+
http=protos.RpcHttp(
98+
status_code=str(status),
99+
headers=headers,
100+
is_raw=True,
101+
body=body))
102+
117103
raise TypeError(
118104
f'unable to encode outgoing TypedData: '
119105
f'unsupported Python type: {type(o)}')
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"scriptFile": "main.py",
3+
"disabled": false,
4+
"bindings": [
5+
{
6+
"authLevel": "anonymous",
7+
"type": "httpTrigger",
8+
"direction": "in",
9+
"name": "req"
10+
},
11+
{
12+
"type": "http",
13+
"direction": "out",
14+
"name": "$return"
15+
}
16+
]
17+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import azure.functions as azf
2+
3+
4+
def main(req: azf.HttpRequest):
5+
return azf.HttpResponse('<h1>Hello World™</h1>',
6+
mimetype='text/html')
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"scriptFile": "main.py",
3+
"disabled": false,
4+
"bindings": [
5+
{
6+
"authLevel": "anonymous",
7+
"type": "httpTrigger",
8+
"direction": "in",
9+
"name": "req"
10+
},
11+
{
12+
"type": "http",
13+
"direction": "out",
14+
"name": "$return"
15+
}
16+
]
17+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import azure.functions as azf
2+
3+
4+
def main(req: azf.HttpRequest):
5+
return azf.HttpResponse('bye', status_code=404)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"scriptFile": "main.py",
3+
"disabled": false,
4+
"bindings": [
5+
{
6+
"authLevel": "anonymous",
7+
"type": "httpTrigger",
8+
"direction": "in",
9+
"name": "req"
10+
},
11+
{
12+
"type": "http",
13+
"direction": "out",
14+
"name": "$return"
15+
}
16+
]
17+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import azure.functions as azf
2+
3+
4+
def main(req: azf.HttpRequest):
5+
return azf.HttpResponse(
6+
status_code=302,
7+
headers={'location': 'return_http'})

0 commit comments

Comments
 (0)