Thanks to visit codestin.com
Credit goes to github.com

Skip to content
This repository was archived by the owner on Aug 26, 2024. It is now read-only.

Commit 20f67ca

Browse files
Merge pull request #2 from encode/add-codec
Add HALCodec
2 parents 119790b + 31bf007 commit 20f67ca

File tree

6 files changed

+316
-4
lines changed

6 files changed

+316
-4
lines changed

hal_codec/__init__.py

Lines changed: 208 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,208 @@
1-
__version__ = "0.0.1"
1+
from collections import OrderedDict
2+
from coreapi.codecs.base import BaseCodec, _get_string, _get_dict, _get_bool
3+
from coreapi.compat import force_bytes, urlparse
4+
from coreapi.compat import COMPACT_SEPARATORS, VERBOSE_SEPARATORS
5+
from coreapi.document import Document, Link, Array, Object, Field, Error
6+
from coreapi.exceptions import ParseError
7+
import json
8+
import uritemplate
9+
10+
11+
__version__ = "0.0.2"
12+
13+
14+
def _is_array_containing_instance(value, datatype):
15+
return isinstance(value, Array) and value and isinstance(value[0], datatype)
16+
17+
18+
def _is_map_containing_instance(value, datatype):
19+
return isinstance(value, Object) and value and isinstance(value[0], datatype)
20+
21+
22+
def _document_to_primative(node, base_url=None):
23+
if isinstance(node, Document):
24+
url = urlparse.urljoin(base_url, node.url)
25+
links = OrderedDict()
26+
embedded = OrderedDict()
27+
data = OrderedDict()
28+
29+
self_link = OrderedDict()
30+
self_link['href'] = url
31+
if node.title:
32+
self_link['title'] = node.title
33+
links['self'] = self_link
34+
35+
for key, value in node.items():
36+
if isinstance(value, Link):
37+
links[key] = _document_to_primative(value, base_url=url)
38+
elif _is_array_containing_instance(value, Link):
39+
links[key] = [
40+
_document_to_primative(item, base_url=url)
41+
for item in value
42+
if isinstance(item, Link)
43+
]
44+
elif _is_map_containing_instance(value, Link):
45+
links[key] = {
46+
key: _document_to_primative(val, base_url=url)
47+
for key, val in value.items()
48+
if isinstance(val, Link)
49+
}
50+
elif isinstance(value, Document):
51+
embedded[key] = _document_to_primative(value, base_url=url)
52+
elif _is_array_containing_instance(value, Document):
53+
embedded[key] = [
54+
_document_to_primative(item, base_url=url)
55+
for item in value
56+
if isinstance(item, Document)
57+
]
58+
elif key not in ('_links', '_embedded'):
59+
data[key] = _document_to_primative(value)
60+
61+
ret = OrderedDict()
62+
ret['_links'] = links
63+
ret.update(data)
64+
if embedded:
65+
ret['_embedded'] = embedded
66+
return ret
67+
68+
elif isinstance(node, Link):
69+
url = urlparse.urljoin(base_url, node.url)
70+
ret = OrderedDict()
71+
ret['href'] = url
72+
templated = [field.name for field in node.fields if field.location == 'path']
73+
if templated:
74+
ret['href'] += "{?%s}" % ','.join([name for name in templated])
75+
ret['templated'] = True
76+
return ret
77+
78+
elif isinstance(node, Array):
79+
return [
80+
_document_to_primative(item) for item in node
81+
]
82+
83+
elif isinstance(node, Object):
84+
return OrderedDict([
85+
(key, _document_to_primative(value)) for key, value in node.items()
86+
if not isinstance(value, (Document, Link))
87+
])
88+
89+
elif isinstance(node, Error):
90+
return OrderedDict([
91+
(key, _document_to_primative(value)) for key, value in node.items()
92+
])
93+
94+
return node
95+
96+
97+
def _parse_link(data, base_url=None):
98+
url = _get_string(data, 'href')
99+
url = urlparse.urljoin(base_url, url)
100+
if _get_bool(data, 'templated'):
101+
fields = [
102+
Field(name, location='path') for name in uritemplate.variables(url)
103+
]
104+
else:
105+
fields = None
106+
return Link(url=url, fields=fields)
107+
108+
109+
def _map_to_coreapi_key(key):
110+
# HAL uses 'rel' values to index links and nested resources.
111+
if key.startswith('http://') or key.startswith('https://'):
112+
# Fully qualified URL - just use last portion of the path.
113+
return urlparse.urlsplit(key).path.split('/')[-1]
114+
elif ':' in key:
115+
# A curried 'rel' value. Use the named portion.
116+
return key.split(':', 1)[1]
117+
# A reserved 'rel' value, such as "next".
118+
return key
119+
120+
121+
def _parse_document(data, base_url=None):
122+
links = _get_dict(data, '_links')
123+
embedded = _get_dict(data, '_embedded')
124+
125+
self = _get_dict(links, 'self')
126+
url = _get_string(self, 'href')
127+
url = urlparse.urljoin(base_url, url)
128+
title = _get_string(self, 'title')
129+
130+
content = {}
131+
132+
for key, value in links.items():
133+
if key in ('self', 'curies'):
134+
continue
135+
136+
key = _map_to_coreapi_key(key)
137+
138+
if isinstance(value, list):
139+
if value and 'name' in value[0]:
140+
# Lists of named links should be represented inside a map.
141+
content[key] = {
142+
item['name']: _parse_link(item, base_url)
143+
for item in value
144+
if 'name' in item
145+
}
146+
else:
147+
# Lists of named links should be represented inside a list.
148+
content[key] = [
149+
_parse_link(item, base_url)
150+
for item in value
151+
]
152+
elif isinstance(value, dict):
153+
# Single link instance.
154+
content[key] = _parse_link(value, base_url)
155+
156+
# Embedded resources.
157+
for key, value in embedded.items():
158+
key = _map_to_coreapi_key(key)
159+
if isinstance(value, list):
160+
content[key] = [_parse_document(item, base_url=url) for item in value]
161+
elif isinstance(value, dict):
162+
content[key] = _parse_document(value, base_url=url)
163+
164+
# Data.
165+
for key, value in data.items():
166+
if key not in ('_embedded', '_links'):
167+
content[key] = value
168+
169+
return Document(url, title, content)
170+
171+
172+
class HALCodec(BaseCodec):
173+
media_type = "application/hal+json"
174+
175+
def dump(self, document, indent=False, **kwargs):
176+
"""
177+
Takes a document and returns a bytestring.
178+
"""
179+
if indent:
180+
options = {
181+
'ensure_ascii': False,
182+
'indent': 4,
183+
'separators': VERBOSE_SEPARATORS
184+
}
185+
else:
186+
options = {
187+
'ensure_ascii': False,
188+
'indent': None,
189+
'separators': COMPACT_SEPARATORS
190+
}
191+
192+
data = _document_to_primative(document)
193+
return force_bytes(json.dumps(data, **options))
194+
195+
def load(self, bytes, base_url=None):
196+
"""
197+
Takes a bytestring and returns a document.
198+
"""
199+
try:
200+
data = json.loads(bytes.decode('utf-8'))
201+
except ValueError as exc:
202+
raise ParseError('Malformed JSON. %s' % exc)
203+
204+
doc = _parse_document(data, base_url)
205+
if not isinstance(doc, Document):
206+
raise ParseError('Top level node must be a document.')
207+
208+
return doc

requirements-unfrozen.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# Package requirements
2+
coreapi
3+
14
# Testing requirements
25
coverage
36
flake8

requirements.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1+
click==6.6
2+
coreapi==1.24.0
13
coverage==4.1
24
flake8==2.6.2
5+
itypes==1.1.0
6+
Jinja2==2.8
7+
MarkupSafe==0.23
38
mccabe==0.5.0
49
py==1.4.31
510
pycodestyle==2.0.0
611
pyflakes==1.2.3
712
pytest==2.9.2
13+
requests==2.10.0
14+
simplejson==3.8.2
15+
uritemplate==0.6

setup.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,5 +68,10 @@ def get_package_data(package):
6868
'Operating System :: OS Independent',
6969
'Programming Language :: Python',
7070
'Programming Language :: Python :: 3',
71-
]
71+
],
72+
entry_points={
73+
'coreapi.codecs': [
74+
'hal=hal_codec.HALCodec',
75+
]
76+
}
7277
)

tests/test_codec.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
from coreapi import Document, Link, Field
2+
from hal_codec import HALCodec
3+
4+
5+
hal_bytestring = b'''{
6+
"_links": {
7+
"self": {"href": "/orders"},
8+
"curies": [{"name": "ea", "href": "http://example.com/docs/rels/{rel}", "templated": true}],
9+
"next": {"href": "/orders?page=2"},
10+
"ea:find": {
11+
"href": "/orders{?id}",
12+
"templated": true
13+
},
14+
"ea:admin": [{
15+
"href": "/admins/2",
16+
"title": "Fred"
17+
}, {
18+
"href": "/admins/5",
19+
"title": "Kate"
20+
}]
21+
},
22+
"currentlyProcessing": 14,
23+
"shippedToday": 20,
24+
"_embedded": {
25+
"ea:order": [{
26+
"_links": {
27+
"self": {"href": "/orders/123"},
28+
"ea:basket": {"href": "/baskets/98712"},
29+
"ea:customer": {"href": "/customers/7809"}
30+
},
31+
"total": 30.00,
32+
"currency": "USD",
33+
"status": "shipped"
34+
}, {
35+
"_links": {
36+
"self": {"href": "/orders/124"},
37+
"ea:basket": {"href": "/baskets/97213"},
38+
"ea:customer": {"href": "/customers/12369"}
39+
},
40+
"total": 20.00,
41+
"currency": "USD",
42+
"status": "processing"
43+
}]
44+
}
45+
}'''
46+
47+
48+
hal_document = Document(
49+
url=u'/orders',
50+
title='',
51+
content={
52+
u'admin': [
53+
Link(url=u'/admins/2'),
54+
Link(url=u'/admins/5')
55+
],
56+
u'currentlyProcessing': 14,
57+
u'order': [
58+
Document(
59+
url=u'/orders/123',
60+
title='',
61+
content={
62+
u'currency': u'USD',
63+
u'status': u'shipped',
64+
u'total': 30.0,
65+
u'basket': Link(url=u'/baskets/98712'),
66+
u'customer': Link(url=u'/customers/7809')
67+
}
68+
),
69+
Document(
70+
url=u'/orders/124',
71+
title='',
72+
content={
73+
u'currency': u'USD',
74+
u'status': u'processing',
75+
u'total': 20.0,
76+
u'basket': Link(url=u'/baskets/97213'),
77+
u'customer': Link(url=u'/customers/12369')
78+
}
79+
)
80+
],
81+
u'shippedToday': 20,
82+
u'find': Link(url=u'/orders{?id}', fields=[Field(u'id', location='path')]),
83+
u'next': Link(url=u'/orders?page=2')
84+
}
85+
)
86+
87+
88+
def test_load():
89+
codec = HALCodec()
90+
doc = codec.load(hal_bytestring)
91+
assert doc == hal_document

tests/test_example.py

Lines changed: 0 additions & 2 deletions
This file was deleted.

0 commit comments

Comments
 (0)