Skip to content

Commit 715b0bd

Browse files
Cleanup client/transport interface
1 parent e2b0dfc commit 715b0bd

9 files changed

Lines changed: 191 additions & 216 deletions

File tree

coreapi/__init__.py

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# coding: utf-8
2+
from coreapi.codecs import dump, load, negotiate_decoder, negotiate_encoder
23
from coreapi.client import Client
34
from coreapi.document import Array, Document, Link, Object, Error, Field
45
from coreapi.exceptions import ParseError, TransportError, ErrorMessage
@@ -10,22 +11,12 @@
1011
'Array', 'Document', 'Link', 'Object', 'Error', 'Field',
1112
'ParseError', 'NotAcceptable', 'TransportError', 'ErrorMessage',
1213
'Client',
13-
'negotiate_encoder', 'negotiate_decoder',
14-
'get', 'action', 'reload', 'load', 'dump',
14+
'load', 'dump', 'negotiate_encoder', 'negotiate_decoder',
15+
'get', 'action', 'reload',
1516
'codecs', 'history', 'transports'
1617
]
1718

1819

19-
def negotiate_encoder(accept=None):
20-
client = Client()
21-
return client.negotiate_encoder(accept)
22-
23-
24-
def negotiate_decoder(content_type=None):
25-
client = Client()
26-
return client.negotiate_decoder(content_type)
27-
28-
2920
def get(url):
3021
client = Client()
3122
return client.get(url)
@@ -39,13 +30,3 @@ def action(document, keys, params=None, action=None, inplace=None):
3930
def reload(document):
4031
client = Client()
4132
return client.reload(document)
42-
43-
44-
def load(bytestring, content_type=None):
45-
client = Client()
46-
return client.load(bytestring, content_type=content_type)
47-
48-
49-
def dump(document, accept=None, **kwargs):
50-
client = Client()
51-
return client.dump(document, accept=accept, **kwargs)

coreapi/client.py

Lines changed: 22 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
from coreapi.codecs import CoreJSONCodec, HALCodec, HTMLCodec, PlainTextCodec
2-
from coreapi.compat import string_types, urlparse
1+
from coreapi.codecs import default_decoders
2+
from coreapi.compat import string_types
33
from coreapi.document import Document, Link
4-
from coreapi.exceptions import LinkLookupError, NotAcceptable, UnsupportedContentType, TransportError
5-
from coreapi.transports import HTTPTransport
4+
from coreapi.exceptions import LinkLookupError
5+
from coreapi.transports import default_transports, determine_transport
66
import collections
77
import itypes
88

99

1010
LinkAncestor = collections.namedtuple('LinkAncestor', ['document', 'keys'])
1111

1212

13-
def lookup_link(document, keys):
13+
def _lookup_link(document, keys):
1414
"""
1515
Validates that keys looking up a link are correct.
1616
@@ -53,30 +53,13 @@ def lookup_link(document, keys):
5353

5454

5555
class Client(itypes.Object):
56-
def __init__(self, codecs=None, transports=None):
57-
if codecs is None:
58-
codecs = [CoreJSONCodec(), HALCodec(), HTMLCodec(), PlainTextCodec()]
56+
def __init__(self, decoders=None, transports=None):
57+
if decoders is None:
58+
decoders = default_decoders
5959
if transports is None:
60-
transports = [HTTPTransport()]
61-
62-
self._codecs = itypes.List(codecs)
60+
transports = default_transports
61+
self._decoders = itypes.List(decoders)
6362
self._transports = itypes.List(transports)
64-
self._decoders = [
65-
codec for codec in codecs
66-
if not getattr(codec.load, 'not_implemented', False)
67-
]
68-
self._encoders = [
69-
codec for codec in codecs
70-
if not getattr(codec.dump, 'not_implemented', False)
71-
]
72-
73-
@property
74-
def codecs(self):
75-
return self._codecs
76-
77-
@property
78-
def encoders(self):
79-
return self._encoders
8063

8164
@property
8265
def decoders(self):
@@ -86,123 +69,34 @@ def decoders(self):
8669
def transports(self):
8770
return self._transports
8871

89-
def get_accept_header(self):
90-
"""
91-
Return an 'Accept' header for the given codecs.
92-
"""
93-
return ', '.join([codec.media_type for codec in self.decoders])
94-
95-
def negotiate_decoder(self, content_type=None):
96-
"""
97-
Given the value of a 'Content-Type' header, return the appropriate
98-
codec registered to decode the request content.
99-
"""
100-
if content_type is None:
101-
return self.decoders[0]
102-
103-
content_type = content_type.split(';')[0].strip().lower()
104-
for codec in self.decoders:
105-
if codec.media_type == content_type:
106-
break
107-
else:
108-
msg = "Unsupported media in Content-Type header '%s'" % content_type
109-
raise UnsupportedContentType(msg)
110-
111-
return codec
112-
113-
def negotiate_encoder(self, accept=None):
114-
"""
115-
Given the value of a 'Accept' header, return a two tuple of the appropriate
116-
content type and codec registered to encode the response content.
117-
"""
118-
if accept is None:
119-
return self.encoders[0]
120-
121-
acceptable = set([
122-
item.split(';')[0].strip().lower()
123-
for item in accept.split(',')
124-
])
125-
126-
for codec in self.encoders:
127-
if codec.media_type in acceptable:
128-
return codec
129-
130-
for codec in self.encoders:
131-
if codec.media_type.split('/')[0] + '/*' in acceptable:
132-
return codec
133-
134-
if '*/*' in acceptable:
135-
return self.encoders[0]
136-
137-
msg = "Unsupported media in Accept header '%s'" % accept
138-
raise NotAcceptable(msg)
139-
140-
def determine_transport(self, url):
141-
"""
142-
Given a URL determine the appropriate transport instance.
143-
"""
144-
url_components = urlparse.urlparse(url)
145-
scheme = url_components.scheme.lower()
146-
netloc = url_components.netloc
147-
148-
if not scheme:
149-
raise TransportError("URL missing scheme '%s'." % url)
150-
151-
if not netloc:
152-
raise TransportError("URL missing hostname '%s'." % url)
153-
154-
for transport in self.transports:
155-
if scheme in transport.schemes:
156-
break
157-
else:
158-
raise TransportError("Unsupported URL scheme '%s'." % scheme)
159-
160-
return transport
161-
16272
def get(self, url):
163-
transport = self.determine_transport(url)
16473
link = Link(url, action='get')
165-
return transport.transition(link, client=self)
74+
75+
# Perform the action, and return a new document.
76+
transport = determine_transport(link.url, transports=self.transports)
77+
return transport.transition(link, decoders=self.decoders)
16678

16779
def reload(self, document):
16880
url = document.url
169-
transport = self.determine_transport(url)
17081
link = Link(url, action='get')
171-
return transport.transition(link, client=self)
82+
83+
# Perform the action, and return a new document.
84+
transport = determine_transport(link.url, transports=self.transports)
85+
return transport.transition(link, decoders=self.decoders)
17286

17387
def action(self, document, keys, params=None, action=None, inplace=None):
17488
if isinstance(keys, string_types):
17589
keys = [keys]
17690

177-
if params is None:
178-
params = {}
179-
18091
# Validate the keys and link parameters.
181-
link, link_ancestors = lookup_link(document, keys)
92+
link, link_ancestors = _lookup_link(document, keys)
18293

183-
if action is not None or inplace is not None:
94+
if (action is not None) or (inplace is not None):
18495
# Handle any explicit overrides.
18596
action = link.action if (action is None) else action
18697
inplace = link.inplace if (inplace is None) else inplace
18798
link = Link(link.url, action, inplace, link.fields)
18899

189100
# Perform the action, and return a new document.
190-
transport = self.determine_transport(link.url)
191-
return transport.transition(link, params, client=self, link_ancestors=link_ancestors)
192-
193-
def load(self, bytestring, content_type=None):
194-
"""
195-
Given a bytestring and an optional content_type, return the
196-
parsed Document.
197-
"""
198-
codec = self.negotiate_decoder(content_type)
199-
return codec.load(bytestring)
200-
201-
def dump(self, document, accept=None, **kwargs):
202-
"""
203-
Given a document, and an optional accept header, return a two-tuple of
204-
the selected media type and encoded bytestring.
205-
"""
206-
codec = self.negotiate_encoder(accept)
207-
content = codec.dump(document, **kwargs)
208-
return (codec.media_type, content)
101+
transport = determine_transport(link.url, transports=self.transports)
102+
return transport.transition(link, params, decoders=self.decoders, link_ancestors=link_ancestors)

coreapi/codecs/__init__.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,85 @@
55
from coreapi.codecs.html import HTMLCodec
66
from coreapi.codecs.plaintext import PlainTextCodec
77
from coreapi.codecs.python import PythonCodec
8+
from coreapi.exceptions import NotAcceptable, UnsupportedContentType
9+
import itypes
810

911

1012
__all__ = [
1113
'BaseCodec', 'CoreJSONCodec', 'HALCodec', 'HTMLCodec', 'PlainTextCodec', 'PythonCodec',
1214
]
15+
16+
default_decoders = itypes.List([CoreJSONCodec(), HALCodec()])
17+
default_encoders = itypes.List([CoreJSONCodec(), HALCodec(), HTMLCodec(), PlainTextCodec()])
18+
19+
20+
def negotiate_decoder(content_type=None, decoders=None):
21+
"""
22+
Given the value of a 'Content-Type' header, return the appropriate
23+
codec registered to decode the request content.
24+
"""
25+
if decoders is None:
26+
decoders = default_decoders
27+
28+
if content_type is None:
29+
return decoders[0]
30+
31+
content_type = content_type.split(';')[0].strip().lower()
32+
for codec in decoders:
33+
if codec.media_type == content_type:
34+
break
35+
else:
36+
msg = "Unsupported media in Content-Type header '%s'" % content_type
37+
raise UnsupportedContentType(msg)
38+
39+
return codec
40+
41+
42+
def negotiate_encoder(accept=None, encoders=None):
43+
"""
44+
Given the value of a 'Accept' header, return a two tuple of the appropriate
45+
content type and codec registered to encode the response content.
46+
"""
47+
if encoders is None:
48+
encoders = default_encoders
49+
50+
if accept is None:
51+
return encoders[0]
52+
53+
acceptable = set([
54+
item.split(';')[0].strip().lower()
55+
for item in accept.split(',')
56+
])
57+
58+
for codec in encoders:
59+
if codec.media_type in acceptable:
60+
return codec
61+
62+
for codec in encoders:
63+
if codec.media_type.split('/')[0] + '/*' in acceptable:
64+
return codec
65+
66+
if '*/*' in acceptable:
67+
return encoders[0]
68+
69+
msg = "Unsupported media in Accept header '%s'" % accept
70+
raise NotAcceptable(msg)
71+
72+
73+
def load(bytestring, content_type=None, base_url=None, decoders=None):
74+
"""
75+
Given a bytestring and an optional content_type, return the
76+
parsed Document.
77+
"""
78+
codec = negotiate_decoder(content_type, decoders=decoders)
79+
return codec.load(bytestring, base_url=base_url)
80+
81+
82+
def dump(document, accept=None, encoders=None, **kwargs):
83+
"""
84+
Given a document, and an optional accept header, return a two-tuple of
85+
the selected media type and encoded bytestring.
86+
"""
87+
codec = negotiate_encoder(accept, encoders=encoders)
88+
content = codec.dump(document, **kwargs)
89+
return (codec.media_type, content)

coreapi/codecs/base.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,11 @@ def _get_bool(item, key, default=False):
2424
return value if isinstance(value, bool) else default
2525

2626

27-
def _mark_as_not_implemented(method):
28-
# Mark the method as not implemented, for the purposes for determining
29-
# if a codec supports encoding only, decoding only, or both.
30-
method.not_implemented = True
31-
return method
32-
33-
3427
class BaseCodec(itypes.Object):
3528
media_type = None
3629

37-
@_mark_as_not_implemented
3830
def load(self, bytes, base_url=None):
3931
raise NotImplementedError() # pragma: nocover
4032

41-
@_mark_as_not_implemented
4233
def dump(self, document, **kwargs):
4334
raise NotImplementedError() # pragma: nocover

coreapi/transports/__init__.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,34 @@
11
# coding: utf-8
2+
from coreapi.compat import urlparse
3+
from coreapi.exceptions import TransportError
24
from coreapi.transports.base import BaseTransport
35
from coreapi.transports.http import HTTPTransport
6+
import itypes
47

58

69
__all__ = [
710
'BaseTransport', 'HTTPTransport'
811
]
12+
13+
default_transports = itypes.List([HTTPTransport()])
14+
15+
16+
def determine_transport(url, transports=default_transports):
17+
"""
18+
Given a URL determine the appropriate transport instance.
19+
"""
20+
url_components = urlparse.urlparse(url)
21+
scheme = url_components.scheme.lower()
22+
netloc = url_components.netloc
23+
24+
if not scheme:
25+
raise TransportError("URL missing scheme '%s'." % url)
26+
27+
if not netloc:
28+
raise TransportError("URL missing hostname '%s'." % url)
29+
30+
for transport in transports:
31+
if scheme in transport.schemes:
32+
return transport
33+
34+
raise TransportError("Unsupported URL scheme '%s'." % scheme)

coreapi/transports/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@
55
class BaseTransport(itypes.Object):
66
schemes = None
77

8-
def transition(self, link, params=None, client=None, link_ancestors=None):
8+
def transition(self, link, params=None, decoders=None, link_ancestors=None):
99
raise NotImplementedError() # pragma: nocover

0 commit comments

Comments
 (0)