8000 Rationalize data codecs and errors · core-api/python-client@d828fb5 · GitHub
[go: up one dir, main page]

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

Commit d828fb5

Browse files
committed
Rationalize data codecs and errors
1 parent bca073f commit d828fb5

File tree

7 files changed

+103
-57
lines changed

7 files changed

+103
-57
lines changed

coreapi/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def _lookup_link(document, keys):
3333
for idx, key in enumerate(keys):
3434
try:
3535
node = node[key]
36-
except (KeyError, IndexError):
36+
except (KeyError, IndexError, TypeError):
3737
index_string = ''.join('[%s]' % repr(key).strip('u') for key in keys)
3838
msg = 'Index %s did not reference a link. Key %s was not found.'
3939
raise LinkLookupError(msg % (index_string, repr(key).strip('u')))

coreapi/codecs/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from coreapi.codecs.hal import HALCodec
55
from coreapi.codecs.html import HTMLCodec
66
from coreapi.codecs.hyperschema import HyperschemaCodec
7+
from coreapi.codecs.jsondata import JSONCodec
78
from coreapi.codecs.plaintext import PlainTextCodec
89
from coreapi.codecs.python import PythonCodec
910
from coreapi.exceptions import NotAcceptable, UnsupportedContentType
@@ -12,10 +13,10 @@
1213

1314
__all__ = [
1415
'BaseCodec', 'CoreJSONCodec', 'HALCodec', 'HTMLCodec', 'HyperschemaCodec',
15-
'PlainTextCodec', 'PythonCodec',
16+
'JSONCodec', 'PlainTextCodec', 'PythonCodec',
1617
]
1718

18-
default_decoders = itypes.List([CoreJSONCodec(), HALCodec()])
19+
default_decoders = itypes.List([CoreJSONCodec(), HALCodec(), HyperschemaCodec(), JSONCodec()])
1920
default_encoders = itypes.List([CoreJSONCodec(), HALCodec(), HTMLCodec(), PlainTextCodec()])
2021

2122

coreapi/codecs/hyperschema.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def _primative_to_document(data, base_url):
7676

7777

7878
class HyperschemaCodec(BaseCodec):
79-
media_type = 'application/vnd.heroku+json; version=3'
79+
media_type = 'application/schema+json'
8080

8181
def load(self, bytes, base_url=None):
8282
"""

coreapi/codecs/jsondata.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# coding: utf-8
2+
from coreapi.codecs.base import BaseCodec
3+
from coreapi.exceptions import ParseError
4+
import json
5+
6+
7+
class JSONCodec(BaseCodec):
8+
media_type = 'application/json'
9+
10+
def load(self, bytes, base_url=None):
11+
"""
12+
Return raw JSON data.
13+
"""
14+
try:
15+
return json.loads(bytes.decode('utf-8'))
16+
except ValueError as exc:
17+
raise ParseError('Malformed JSON. %s' % exc)

coreapi/commandline.py

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -80,15 +80,19 @@ def get_document():
8080

8181
def set_document(doc):
8282
codec = coreapi.codecs.CoreJSONCodec()
83-
content_type, content = codec.dump(doc)
83+
content = codec.dump(doc)
8484
store = open(document_path, 'wb')
8585
store.write(content)
8686
store.close()
8787

8888

8989
def display(doc):
90-
codec = coreapi.codecs.PlainTextCodec()
91-
return codec.dump(doc, colorize=True)
90+
if isinstance(doc, (coreapi.Document, coreapi.Error)):
91+
codec = coreapi.codecs.PlainTextCodec()
92+
return codec.dump(doc, colorize=True)
93+
if doc is None:
94+
return ''
95+
return json.dumps(doc, indent=4, ensure_ascii=False, separators=coreapi.compat.VERBOSE_SEPARATORS)
9296

9397

9498
# Core commands
@@ -123,10 +127,11 @@ def get(url):
123127
except coreapi.exceptions.ErrorMessage as exc:
124128
click.echo(display(exc.error))
125129
sys.exit(1)
126-
history = history.add(doc)
127130
click.echo(display(doc))
128-
set_document(doc)
129-
set_history(history)
131+
if isinstance(doc, coreapi.Document):
132+
history = history.add(doc)
133+
set_document(doc)
134+
set_history(history)
130135

131136

132137
@click.command(help='Clear the active document and other state.\n\nThis includes the current document, history, credentials, headers and bookmarks.')
@@ -217,10 +222,11 @@ def action(path, param, action, inplace):
217222
except coreapi.exceptions.LinkLookupError as exc:
218223
click.echo(exc)
219224
sys.exit(1)
220-
history = history.add(doc)
221225
click.echo(display(doc))
222-
set_document(doc)
223-
set_history(history)
226+
if isinstance(doc, coreapi.Document):
227+
history = history.add(doc)
228+
set_document(doc)
229+
set_history(history)
224230

225231

226232
@click.command(help='Reload the current document.')
@@ -237,10 +243,11 @@ def reload_document():
237243
except coreapi.exceptions.ErrorMessage as exc:
238244
click.echo(display(exc.error))
239245
sys.exit(1)
240-
history = history.add(doc)
241246
click.echo(display(doc))
242-
set_document(doc)
243-
set_history(history)
247+
if isinstance(doc, coreapi.Document):
248+
history = history.add(doc)
249+
set_document(doc)
250+
set_history(history)
244251

245252

246253
# Credentials
@@ -430,14 +437,20 @@ def bookmarks_get(name):
430437
if bookmark is None:
431438
click.echo('Bookmark "%s" does not exist.' % name)
432439
return
440+
url = bookmark['url']
433441

434442
client = get_client()
435443
history = get_history()
436-
doc = client.get(bookmark['url'])
437-
history = history.add(doc)
444+
try:
445+
doc = client.get(url)
446+
except coreapi.exceptions.ErrorMessage as exc:
447+
click.echo(display(exc.error))
448+
sys.exit(1)
438449
click.echo(display(doc))
439-
set_document(doc)
440-
set_history(history)
450+
if isinstance(doc, coreapi.Document):
451+
history = history.add(doc)
452+
set_document(doc)
453+
set_history(history)
441454

442455

443456
# History
@@ -481,10 +494,15 @@ def history_back():
481494
click.echo("Currently at oldest point in history. Cannot navigate back.")
482495
return
483496
doc, history = history.back()
484-
doc = client.reload(doc)
497+
try:
498+
doc = client.reload(doc)
499+
except coreapi.exceptions.ErrorMessage as exc:
500+
click.echo(display(exc.error))
501+
sys.exit(1)
485502
click.echo(display(doc))
486-
set_history(history)
487-
set_document(doc)
503+
if isinstance(doc, coreapi.Document):
504+
set_document(doc)
505+
set_history(history)
488506

489507

490508
@click.command(help="Navigate forward through the browser history.")
@@ -495,10 +513,15 @@ def history_forward():
495513
click.echo("Currently at most recent point in history. Cannot navigate forward.")
496514
return
497515
doc, history = history.forward()
498-
doc = client.reload(doc)
516+
try:
517+
doc = client.reload(doc)
518+
except coreapi.exceptions.ErrorMessage as exc:
519+
click.echo(display(exc.error))
520+
sys.exit(1)
499521
click.echo(display(doc))
500-
set_history(history)
501-
set_document(doc)
522+
if isinstance(doc, coreapi.Document):
523+
set_document(doc)
524+
set_history(history)
502525

503526

504527
client.add_command(get)

coreapi/transports/http.py

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from coreapi.codecs import default_decoders, negotiate_decoder
55
from coreapi.compat import urlparse
66
from coreapi.document import Document, Object, Link, Array, Error
7-
from coreapi.exceptions import ErrorMessage, UnsupportedContentType
7+
from coreapi.exceptions import ErrorMessage
88
from coreapi.transports.base import BaseTransport
99
import requests
1010
import itypes
@@ -17,11 +17,13 @@ def _coerce_to_error_content(node):
1717
# If we get a 4xx or 5xx response with a Document, then coerce it
1818
# into plain data.
1919
if isinstance(node, (Document, Object)):
20+
# Strip Links from Documents, treat Documents as plain dicts.
2021
return OrderedDict([
2122
(key, _coerce_to_error_content(value))
2223
for key, value in node.data.items()
2324
])
2425
elif isinstance(node, Array):
26+
# Strip Links from Arrays.
2527
return [
2628
_coerce_to_error_content(item)
2729
for item in node
@@ -30,6 +32,19 @@ def _coerce_to_error_content(node):
3032
return node
3133

3234

35+
def _coerce_to_error(obj, default_title):
36+
if isinstance(obj, Document):
37+
return Error(
38+
title=obj.title or default_title,
39+
content=_coerce_to_error_content(obj)
40+
)
41+
elif isinstance(obj, dict):
42+
return Error(title=default_title, content=obj)
43+
elif isinstance(obj, list):
44+
return Error(title=default_title, content={'messages': obj})
45+
return Error(title=default_title, content={'message': obj})
46+
47+
3348
def _get_accept_header(decoders=None):
3449
if decoders is None:
3550
decoders = default_decoders
@@ -60,32 +75,14 @@ def transition(self, link, params=None, decoders=None, link_ancestors=None):
6075
url = self.expand_path_params(link.url, path_params)
6176
headers = self.get_headers(url, decoders)
6277
response = self.make_http_request(url, method, headers, query_params, form_params)
63-
is_error = response.status_code >= 400 and response.status_code <= 599
64-
try:
65-
document = self.load_document(decoders, response)
66-
except UnsupportedContentType:
67-
content_type = response.headers.get('content-type').split(';')[0]
68-
if is_error and content_type == 'application/json':
69-
content = json.loads(response.content)
70-
document = Error(title=response.reason, content=content)
71-
else:
72-
raise
78+
document = self.load_document(response, decoders)
7379

74-
if isinstance(document, Document) and is_error:
75-
# Coerce 4xx and 5xx codes into errors.
76-
document = Error(
77-
title=document.title,
78-
content=_coerce_to_error_content(document)
79-
)
80+
if isinstance(document, Document) and link_ancestors:
81+
document = self.handle_inplace_replacements(document, link, link_ancestors)
8082

8183
if isinstance(document, Error):
8284
raise ErrorMessage(document)
8385

84-
if link_ancestors:
85-
document = self.handle_inplace_replacements(document, link, link_ancestors)
86-
87-
if document is None:
88-
document = Document(url=response.url)
8986
return document
9087

9188
def get_http_method(self, action):
@@ -162,15 +159,25 @@ def make_http_request(self, url, method, headers=None, query_params=None, form_p
162159

163160
return requests.request(method, url, **opts)
164161

165-
def load_document(self, decoders, response):
162+
def load_document(self, response, decoders=None):
166163
"""
167164
Given an HTTP response, return the decoded Core API document.
168165
"""
169-
if not response.content:
170-
return None
171-
content_type = response.headers.get('content-type')
172-
codec = negotiate_decoder(content_type, decoders=decoders)
173-
return codec.load(response.content, base_url=response.url)
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
174181

175182
def handle_inplace_replacements(self, document, link, link_ancestors):
176183
"""

tests/test_transport.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,7 @@ def mockreturn(method, url, **opts):
101101

102102
link = Link(url='http://example.org', action='delete')
103103
doc = http.transition(link)
104-
assert doc.url == 'http://example.org'
105-
assert not doc.items()
106-
assert not doc.title
104+
assert doc is None
107105

108106

109107
# Test credentials

0 commit comments

Comments
 (0)
0