Skip to content

Commit 1fe341e

Browse files
Merge pull request neovim#62 from core-api/internal-refactoring
Internal refactoring
2 parents 277e694 + 87ff51a commit 1fe341e

File tree

4 files changed

+154
-165
lines changed

4 files changed

+154
-165
lines changed

coreapi/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from coreapi import codecs, history, transports
77

88

9-
__version__ = '1.13.1'
9+
__version__ = '1.13.2'
1010
__all__ = [
1111
'Array', 'Document', 'Link', 'Object', 'Error', 'Field',
1212
'ParseError', 'NotAcceptable', 'TransportError', 'ErrorMessage',

coreapi/transports/http.py

Lines changed: 151 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,100 @@
1212
import uritemplate
1313

1414

15+
def _get_http_method(action):
16+
if not action:
17+
return 'GET'
18+
return action.upper()
19+
20+
21+
def _seperate_params(method, fields, params=None):
22+
"""
23+
Seperate the params into their location types: path, query, or form.
24+
"""
25+
if params is None:
26+
return ({}, {}, {})
27+
28+
field_map = {field.name: field for field in fields}
29+
path_params = {}
30+
query_params = {}
31+
form_params = {}
32+
for key, value in params.items():
33+
if key not in field_map or not field_map[key].location:
34+
# Default is 'query' for 'GET'/'DELETE', and 'form' others.
35+
location = 'query' if method in ('GET', 'DELETE') else 'form'
36+
else:
37+
location = field_map[key].location
38+
39+
if location == 'path':
40+
path_params[key] = value
41+
elif location == 'query':
42+
query_params[key] = value
43+
else:
44+
form_params[key] = value
45+
46+
return path_params, query_params, form_params
47+
48+
49+
def _expand_path_params(url, path_params):
50+
"""
51+
Given a templated URL and some parameters that have been provided,
52+
expand the URL.
53+
"""
54+
if path_params:
55+
return uritemplate.expand(url, path_params)
56+
return url
57+
58+
59+
def _get_headers(url, decoders=None, credentials=None, extra_headers=None):
60+
"""
61+
Return a dictionary of HTTP headers to use in the outgoing request.
62+
"""
63+
if decoders is None:
64+
decoders = default_decoders
65+
66+
accept = ', '.join([decoder.media_type for decoder in decoders])
67+
68+
headers = {
69+
'accept': accept
70+
}
71+
72+
if credentials:
73+
# Include any authorization credentials relevant to this domain.
74+
url_components = urlparse.urlparse(url)
75+
host = url_components.netloc
76+
if host in credentials:
77+
headers['authorization'] = credentials[host]
78+
79+
if extra_headers:
80+
# Include any custom headers associated with this transport.
81+
headers.update(extra_headers)
82+
83+
return headers
84+
85+
86+
def _make_http_request(url, method, headers=None, query_params=None, form_params=None):
87+
"""
88+
Make an HTTP request and return an HTTP response.
89+
"""
90+
opts = {
91+
"headers": headers or {}
92+
}
93+
94+
if query_params:
95+
opts['params'] = query_params
96+
elif form_params:
97+
opts['data'] = json.dumps(form_params)
98+
opts['headers']['content-type'] = 'application/json'
99+
100+
return requests.request(method, url, **opts)
101+
102+
15103
def _coerce_to_error_content(node):
16-
# Errors should not contain nested documents or links.
17-
# If we get a 4xx or 5xx response with a Document, then coerce it
18-
# into plain data.
104+
"""
105+
Errors should not contain nested documents or links.
106+
If we get a 4xx or 5xx response with a Document, then coerce
107+
the document content into plain data.
108+
"""
19109
if isinstance(node, (Document, Object)):
20110
# Strip Links from Documents, treat Documents as plain dicts.
21111
return OrderedDict([
@@ -33,6 +123,9 @@ def _coerce_to_error_content(node):
33123

34124

35125
def _coerce_to_error(obj, default_title):
126+
"""
127+
Given an arbitrary return result, coerce it into an Error instance.
128+
"""
36129
if isinstance(obj, Document):
37130
return Error(
38131
title=obj.title or default_title,
@@ -42,14 +135,53 @@ def _coerce_to_error(obj, default_title):
42135
return Error(title=default_title, content=obj)
43136
elif isinstance(obj, list):
44137
return Error(title=default_title, content={'messages': obj})
138+
elif obj is None:
139+
return Error(title=default_title)
45140
return Error(title=default_title, content={'message': obj})
46141

47142

48-
def _get_accept_header(decoders=None):
49-
if decoders is None:
50-
decoders = default_decoders
143+
def _decode_result(response, decoders=None):
144+
"""
145+
Given an HTTP response, return the decoded Core API document.
146+
"""
147+
if response.content:
148+
# Content returned in response. We should decode it.
149+
content_type = response.headers.get('content-type')
150+
codec = negotiate_decoder(content_type, decoders=decoders)
151+
result = codec.load(response.content, base_url=response.url)
152+
else:
153+
# No content returned in response.
154+
result = None
155+
156+
# Coerce 4xx and 5xx codes into errors.
157+
is_error = response.status_code >= 400 and response.status_code <= 599
158+
if is_error and not isinstance(result, Error):
159+
result = _coerce_to_error(result, default_title=response.reason)
51160

52-
return ', '.join([decoder.media_type for decoder in decoders])
161+
return result
162+
163+
164+
def _handle_inplace_replacements(document, link, link_ancestors):
165+
"""
166+
Given a new document, and the link/ancestors it was created,
167+
determine if we should:
168+
169+
* Make an inline replacement and then return the modified document tree.
170+
* Return the new document as-is.
171+
"""
172+
if link.inplace is None:
173+
inplace = link.action.lower() in ('put', 'patch', 'delete')
174+
else:
175+
inplace = link.inplace
176+
177+
if inplace:
178+
root = link_ancestors[0].document
179+
keys_to_link_parent = link_ancestors[-1].keys
180+
if document is None:
181+
return root.delete_in(keys_to_link_parent)
182+
return root.set_in(keys_to_link_parent, document)
183+
184+
return document
53185

54186

55187
class HTTPTransport(BaseTransport):
@@ -70,133 +202,17 @@ def headers(self):
70202
return self._headers
71203

72204
def transition(self, link, params=None, decoders=None, link_ancestors=None):
73-
method = self.get_http_method(link.action)
74-
path_params, query_params, form_params = self.seperate_params(method, link.fields, params)
75-
url = self.expand_path_params(link.url, path_params)
76-
headers = self.get_headers(url, decoders)
77-
response = self.make_http_request(url, method, headers, query_params, form_params)
78-
document = self.load_document(response, decoders)
79-
80-
if isinstance(document, Document) and link_ancestors:
81-
document = self.handle_inplace_replacements(document, link, link_ancestors)
82-
83-
if isinstance(document, Error):
84-
raise ErrorMessage(document)
85-
86-
return document
87-
88-
def get_http_method(self, action):
89-
if not action:
90-
return 'GET'
91-
return action.upper()
92-
93-
def seperate_params(self, method, fields, params=None):
94-
"""
95-
Seperate the params into their location types: path, query, or form.
96-
"""
97-
if params is None:
98-
return ({}, {}, {})
99-
100-
field_map = {field.name: field for field in fields}
101-
path_params = {}
102-
query_params = {}
103-
form_params = {}
104-
for key, value in params.items():
105-
if key not in field_map or not field_map[key].location:
106-
# Default is 'query' for 'GET'/'DELETE', and 'form' others.
107-
location = 'query' if method in ('GET', 'DELETE') else 'form'
108-
else:
109-
location = field_map[key].location
110-
111-
if location == 'path':
112-
path_params[key] = value
113-
elif location == 'query':
114-
query_params[key] = value
115-
else:
116-
form_params[key] = value
117-
118-
return path_params, query_params, form_params
119-
120-
def expand_path_params(self, url, path_params):
121-
if path_params:
122-
return uritemplate.expand(url, path_params)
123-
return url
124-
125-
def get_headers(self, url, decoders=None):
126-
"""
127-
Return a dictionary of HTTP headers to use in the outgoing request.
128-
"""
129-
headers = {
130-
'accept': _get_accept_header(decoders)
131-
}
132-
133-
if self.credentials:
134-
# Include any authorization credentials relevant to this domain.
135-
url_components = urlparse.urlparse(url)
136-
host = url_components.netloc
137-
if host in self.credentials:
138-
headers['authorization'] = self.credentials[host]
139-
140-
if self.headers:
141-
# Include any custom headers associated with this transport.
142-
headers.update(self.headers)
143-
144-
return headers
145-
146-
def make_http_request(self, url, method, headers=None, query_params=None, form_params=None):
147-
"""
148-
Make an HTTP request and return an HTTP response.
149-
"""
150-
opts = {
151-
"headers": headers or {}
152-
}
153-
154-
if query_params:
155-
opts['params'] = query_params
156-
elif form_params:
157-
opts['data'] = json.dumps(form_params)
158-
opts['headers']['content-type'] = 'application/json'
159-
160-
return requests.request(method, url, **opts)
161-
162-
def load_document(self, response, decoders=None):
163-
"""
164-
Given an HTTP response, return the decoded Core API document.
165-
"""
166-
if response.content:
167-
# Content returned in response. We should decode it.
168-
content_type = response.headers.get('content-type')
169-
codec = negotiate_decoder(content_type, decoders=decoders)
170-
document = codec.load(response.content, base_url=response.url)
171-
else:
172-
# No content returned in response.
173-
document = None
174-
175-
# Coerce 4xx and 5xx codes into errors.
176-
is_error = response.status_code >= 400 and response.status_code <= 599
177-
if is_error and not isinstance(document, Error):
178-
document = _coerce_to_error(document, default_title=response.reason)
179-
180-
return document
181-
182-
def handle_inplace_replacements(self, document, link, link_ancestors):
183-
"""
184-
Given a new document, and the link/ancestors it was created,
185-
determine if we should:
186-
187-
* Make an inline replacement and then return the modified document tree.
188-
* Return the new document as-is.
189-
"""
190-
if link.inplace is None:
191-
inplace = link.action.lower() in ('put', 'patch', 'delete')
192-
else:
193-
inplace = link.inplace
205+
method = _get_http_method(link.action)
206+
path_params, query_params, form_params = _seperate_params(method, link.fields, params)
207+
url = _expand_path_params(link.url, path_params)
208+
headers = _get_headers(url, decoders, self.credentials, self.headers)
209+
response = _make_http_request(url, method, headers, query_params, form_params)
210+
result = _decode_result(response, decoders)
211+
212+
if isinstance(result, Document) and link_ancestors:
213+
result = _handle_inplace_replacements(result, link, link_ancestors)
194214

195-
if inplace:
196-
root = link_ancestors[0].document
197-
keys_to_link_parent = link_ancestors[-1].keys
198-
if document is None:
199-
return root.delete_in(keys_to_link_parent)
200-
return root.set_in(keys_to_link_parent, document)
215+
if isinstance(result, Error):
216+
raise ErrorMessage(result)
201217

202-
return document
218+
return result

tests/test_transitions.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# coding: utf-8
22
from coreapi import Document, Link, Client
33
from coreapi.transports import HTTPTransport
4+
from coreapi.transports.http import _handle_inplace_replacements
45
import pytest
56

67

@@ -17,7 +18,7 @@ def transition(self, link, params=None, decoders=None, link_ancestors=None):
1718
else:
1819
document = None
1920

20-
return self.handle_inplace_replacements(document, link, link_ancestors)
21+
return _handle_inplace_replacements(document, link, link_ancestors)
2122

2223

2324
client = Client(transports=[MockTransport()])

tests/test_transport.py

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -102,31 +102,3 @@ def mockreturn(method, url, **opts):
102102
link = Link(url='http://example.org', action='delete')
103103
doc = http.transition(link)
104104
assert doc is None
105-
106-
107-
# Test credentials
108-
109-
def test_credentials(monkeypatch):
110-
credentials = {'example.org': 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=='}
111-
transport = HTTPTransport(credentials=credentials)
112-
113-
# Requests to example.org include credentials.
114-
headers = transport.get_headers('http://example.org/123')
115-
assert 'authorization' in headers
116-
assert headers['authorization'] == 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=='
117-
118-
# Requests to other.org do not include credentials.
119-
headers = transport.get_headers('http://other.org/123')
120-
assert 'authorization' not in headers
121-
122-
123-
# Test custom headers
124-
125-
def test_headers(monkeypatch):
126-
headers = {'User-Agent': 'Example v1.0'}
127-
transport = HTTPTransport(headers=headers)
128-
129-
# Requests include custom headers.
130-
headers = transport.get_headers('http://example.org/123')
131-
assert 'user-agent' in headers
132-
assert headers['user-agent'] == 'Example v1.0'

0 commit comments

Comments
 (0)