Skip to content
This repository was archived by the owner on Mar 18, 2019. It is now read-only.

Commit 1fe341e

Browse files
committed
Merge pull request #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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy