Support XML request format
blueprint quantum-client-xml Change-Id: I9db8ea7c395909def00d6f25c9c1a98c07fdde68
This commit is contained in:
parent
aa41734347
commit
5117731a6d
@ -1,7 +1,7 @@
|
||||
[DEFAULT]
|
||||
|
||||
# The list of modules to copy from openstack-common
|
||||
modules=setup
|
||||
modules=jsonutils,setup,timeutils
|
||||
|
||||
# The base module to hold the copy of openstack.common
|
||||
base=quantumclient
|
||||
|
@ -14,11 +14,12 @@ else
|
||||
NOAUTH=
|
||||
fi
|
||||
|
||||
FORMAT=" --request-format xml"
|
||||
|
||||
# test the CRUD of network
|
||||
network=mynet1
|
||||
quantum net-create $NOAUTH $network || die "fail to create network $network"
|
||||
temp=`quantum net-list -- --name $network --fields id | wc -l`
|
||||
quantum net-create $FORMAT $NOAUTH $network || die "fail to create network $network"
|
||||
temp=`quantum net-list $FORMAT -- --name $network --fields id | wc -l`
|
||||
echo $temp
|
||||
if [ $temp -ne 5 ]; then
|
||||
die "networks with name $network is not unique or found"
|
||||
@ -26,102 +27,102 @@ fi
|
||||
network_id=`quantum net-list -- --name $network --fields id | tail -n 2 | head -n 1 | cut -d' ' -f 2`
|
||||
echo "ID of network with name $network is $network_id"
|
||||
|
||||
quantum net-show $network || die "fail to show network $network"
|
||||
quantum net-show $network_id || die "fail to show network $network_id"
|
||||
quantum net-show $FORMAT $network || die "fail to show network $network"
|
||||
quantum net-show $FORMAT $network_id || die "fail to show network $network_id"
|
||||
|
||||
quantum net-update $network --admin_state_up False || die "fail to update network $network"
|
||||
quantum net-update $network_id --admin_state_up True || die "fail to update network $network_id"
|
||||
quantum net-update $FORMAT $network --admin_state_up False || die "fail to update network $network"
|
||||
quantum net-update $FORMAT $network_id --admin_state_up True || die "fail to update network $network_id"
|
||||
|
||||
quantum net-list -c id -- --id fakeid || die "fail to list networks with column selection on empty list"
|
||||
quantum net-list $FORMAT -c id -- --id fakeid || die "fail to list networks with column selection on empty list"
|
||||
|
||||
# test the CRUD of subnet
|
||||
subnet=mysubnet1
|
||||
cidr=10.0.1.3/24
|
||||
quantum subnet-create $NOAUTH $network $cidr --name $subnet || die "fail to create subnet $subnet"
|
||||
tempsubnet=`quantum subnet-list -- --name $subnet --fields id | wc -l`
|
||||
quantum subnet-create $FORMAT $NOAUTH $network $cidr --name $subnet || die "fail to create subnet $subnet"
|
||||
tempsubnet=`quantum subnet-list $FORMAT -- --name $subnet --fields id | wc -l`
|
||||
echo $tempsubnet
|
||||
if [ $tempsubnet -ne 5 ]; then
|
||||
die "subnets with name $subnet is not unique or found"
|
||||
fi
|
||||
subnet_id=`quantum subnet-list -- --name $subnet --fields id | tail -n 2 | head -n 1 | cut -d' ' -f 2`
|
||||
subnet_id=`quantum subnet-list $FORMAT -- --name $subnet --fields id | tail -n 2 | head -n 1 | cut -d' ' -f 2`
|
||||
echo "ID of subnet with name $subnet is $subnet_id"
|
||||
quantum subnet-show $subnet || die "fail to show subnet $subnet"
|
||||
quantum subnet-show $subnet_id || die "fail to show subnet $subnet_id"
|
||||
quantum subnet-show $FORMAT $subnet || die "fail to show subnet $subnet"
|
||||
quantum subnet-show $FORMAT $subnet_id || die "fail to show subnet $subnet_id"
|
||||
|
||||
quantum subnet-update $subnet --dns_namesevers host1 || die "fail to update subnet $subnet"
|
||||
quantum subnet-update $subnet_id --dns_namesevers host2 || die "fail to update subnet $subnet_id"
|
||||
quantum subnet-update $FORMAT $subnet --dns_namesevers host1 || die "fail to update subnet $subnet"
|
||||
quantum subnet-update $FORMAT $subnet_id --dns_namesevers host2 || die "fail to update subnet $subnet_id"
|
||||
|
||||
# test the crud of ports
|
||||
port=myport1
|
||||
quantum port-create $NOAUTH $network --name $port || die "fail to create port $port"
|
||||
tempport=`quantum port-list -- --name $port --fields id | wc -l`
|
||||
quantum port-create $FORMAT $NOAUTH $network --name $port || die "fail to create port $port"
|
||||
tempport=`quantum port-list $FORMAT -- --name $port --fields id | wc -l`
|
||||
echo $tempport
|
||||
if [ $tempport -ne 5 ]; then
|
||||
die "ports with name $port is not unique or found"
|
||||
fi
|
||||
port_id=`quantum port-list -- --name $port --fields id | tail -n 2 | head -n 1 | cut -d' ' -f 2`
|
||||
port_id=`quantum port-list $FORMAT -- --name $port --fields id | tail -n 2 | head -n 1 | cut -d' ' -f 2`
|
||||
echo "ID of port with name $port is $port_id"
|
||||
quantum port-show $port || die "fail to show port $port"
|
||||
quantum port-show $port_id || die "fail to show port $port_id"
|
||||
quantum port-show $FORMAT $port || die "fail to show port $port"
|
||||
quantum port-show $FORMAT $port_id || die "fail to show port $port_id"
|
||||
|
||||
quantum port-update $port --device_id deviceid1 || die "fail to update port $port"
|
||||
quantum port-update $port_id --device_id deviceid2 || die "fail to update port $port_id"
|
||||
quantum port-update $FORMAT $port --device_id deviceid1 || die "fail to update port $port"
|
||||
quantum port-update $FORMAT $port_id --device_id deviceid2 || die "fail to update port $port_id"
|
||||
|
||||
# test quota commands RUD
|
||||
DEFAULT_NETWORKS=10
|
||||
DEFAULT_PORTS=50
|
||||
tenant_id=tenant_a
|
||||
tenant_id_b=tenant_b
|
||||
quantum quota-update --tenant_id $tenant_id --network 30 || die "fail to update quota for tenant $tenant_id"
|
||||
quantum quota-update --tenant_id $tenant_id_b --network 20 || die "fail to update quota for tenant $tenant_id"
|
||||
networks=`quantum quota-list -c network -c tenant_id | grep $tenant_id | awk '{print $2}'`
|
||||
quantum quota-update $FORMAT --tenant_id $tenant_id --network 30 || die "fail to update quota for tenant $tenant_id"
|
||||
quantum quota-update $FORMAT --tenant_id $tenant_id_b --network 20 || die "fail to update quota for tenant $tenant_id"
|
||||
networks=`quantum quota-list $FORMAT -c network -c tenant_id | grep $tenant_id | awk '{print $2}'`
|
||||
if [ $networks -ne 30 ]; then
|
||||
die "networks quota should be 30"
|
||||
fi
|
||||
networks=`quantum quota-list -c network -c tenant_id | grep $tenant_id_b | awk '{print $2}'`
|
||||
networks=`quantum quota-list $FORMAT -c network -c tenant_id | grep $tenant_id_b | awk '{print $2}'`
|
||||
if [ $networks -ne 20 ]; then
|
||||
die "networks quota should be 20"
|
||||
fi
|
||||
networks=`quantum quota-show --tenant_id $tenant_id | grep network | awk -F'|' '{print $3}'`
|
||||
networks=`quantum quota-show $FORMAT --tenant_id $tenant_id | grep network | awk -F'|' '{print $3}'`
|
||||
if [ $networks -ne 30 ]; then
|
||||
die "networks quota should be 30"
|
||||
fi
|
||||
quantum quota-delete --tenant_id $tenant_id || die "fail to delete quota for tenant $tenant_id"
|
||||
networks=`quantum quota-show --tenant_id $tenant_id | grep network | awk -F'|' '{print $3}'`
|
||||
quantum quota-delete $FORMAT --tenant_id $tenant_id || die "fail to delete quota for tenant $tenant_id"
|
||||
networks=`quantum quota-show $FORMAT --tenant_id $tenant_id | grep network | awk -F'|' '{print $3}'`
|
||||
if [ $networks -ne $DEFAULT_NETWORKS ]; then
|
||||
die "networks quota should be $DEFAULT_NETWORKS"
|
||||
fi
|
||||
# update self
|
||||
if [ "t$NOAUTH" = "t" ]; then
|
||||
# with auth
|
||||
quantum quota-update --port 99 || die "fail to update quota for self"
|
||||
ports=`quantum quota-show | grep port | awk -F'|' '{print $3}'`
|
||||
quantum quota-update $FORMAT --port 99 || die "fail to update quota for self"
|
||||
ports=`quantum quota-show $FORMAT | grep port | awk -F'|' '{print $3}'`
|
||||
if [ $ports -ne 99 ]; then
|
||||
die "ports quota should be 99"
|
||||
fi
|
||||
|
||||
ports=`quantum quota-list -c port | grep 99 | awk '{print $2}'`
|
||||
ports=`quantum quota-list $FORMAT -c port | grep 99 | awk '{print $2}'`
|
||||
if [ $ports -ne 99 ]; then
|
||||
die "ports quota should be 99"
|
||||
fi
|
||||
quantum quota-delete || die "fail to delete quota for tenant self"
|
||||
ports=`quantum quota-show | grep port | awk -F'|' '{print $3}'`
|
||||
quantum quota-delete $FORMAT || die "fail to delete quota for tenant self"
|
||||
ports=`quantum quota-show $FORMAT | grep port | awk -F'|' '{print $3}'`
|
||||
if [ $ports -ne $DEFAULT_PORTS ]; then
|
||||
die "ports quota should be $DEFAULT_PORTS"
|
||||
fi
|
||||
else
|
||||
# without auth
|
||||
quantum quota-update --port 100
|
||||
quantum quota-update $FORMAT --port 100
|
||||
if [ $? -eq 0 ]; then
|
||||
die "without valid context on server, quota update command should fail."
|
||||
fi
|
||||
quantum quota-show
|
||||
quantum quota-show $FORMAT
|
||||
if [ $? -eq 0 ]; then
|
||||
die "without valid context on server, quota show command should fail."
|
||||
fi
|
||||
quantum quota-delete
|
||||
quantum quota-delete $FORMAT
|
||||
if [ $? -eq 0 ]; then
|
||||
die "without valid context on server, quota delete command should fail."
|
||||
fi
|
||||
quantum quota-list || die "fail to update quota for self"
|
||||
quantum quota-list $FORMAT || die "fail to update quota for self"
|
||||
fi
|
||||
|
40
quantumclient/common/constants.py
Normal file
40
quantumclient/common/constants.py
Normal file
@ -0,0 +1,40 @@
|
||||
# Copyright (c) 2012 OpenStack, LLC.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
EXT_NS = '_extension_ns'
|
||||
XML_NS_V20 = 'http://openstack.org/quantum/api/v2.0'
|
||||
XSI_NAMESPACE = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
XSI_ATTR = "xsi:nil"
|
||||
XSI_NIL_ATTR = "xmlns:xsi"
|
||||
TYPE_XMLNS = "xmlns:quantum"
|
||||
TYPE_ATTR = "quantum:type"
|
||||
VIRTUAL_ROOT_KEY = "_v_root"
|
||||
|
||||
TYPE_BOOL = "bool"
|
||||
TYPE_INT = "int"
|
||||
TYPE_LONG = "long"
|
||||
TYPE_FLOAT = "float"
|
||||
TYPE_LIST = "list"
|
||||
TYPE_DICT = "dict"
|
||||
|
||||
PLURALS = {'networks': 'network',
|
||||
'ports': 'port',
|
||||
'subnets': 'subnet',
|
||||
'dns_nameservers': 'dns_nameserver',
|
||||
'host_routes': 'host_route',
|
||||
'allocation_pools': 'allocation_pool',
|
||||
'fixed_ips': 'fixed_ip',
|
||||
'extensions': 'extension'}
|
@ -1,7 +1,353 @@
|
||||
from xml.dom import minidom
|
||||
# Copyright 2013 OpenStack LLC.
|
||||
# All Rights Reserved
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
###
|
||||
### Codes from quantum wsgi
|
||||
###
|
||||
|
||||
import logging
|
||||
|
||||
from xml.etree import ElementTree as etree
|
||||
from xml.parsers import expat
|
||||
|
||||
from quantumclient.common import constants
|
||||
from quantumclient.common import exceptions as exception
|
||||
from quantumclient.common import utils
|
||||
from quantumclient.openstack.common import jsonutils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ActionDispatcher(object):
|
||||
"""Maps method name to local methods through action name."""
|
||||
|
||||
def dispatch(self, *args, **kwargs):
|
||||
"""Find and call local method."""
|
||||
action = kwargs.pop('action', 'default')
|
||||
action_method = getattr(self, str(action), self.default)
|
||||
return action_method(*args, **kwargs)
|
||||
|
||||
def default(self, data):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class DictSerializer(ActionDispatcher):
|
||||
"""Default request body serialization"""
|
||||
|
||||
def serialize(self, data, action='default'):
|
||||
return self.dispatch(data, action=action)
|
||||
|
||||
def default(self, data):
|
||||
return ""
|
||||
|
||||
|
||||
class JSONDictSerializer(DictSerializer):
|
||||
"""Default JSON request body serialization"""
|
||||
|
||||
def default(self, data):
|
||||
return jsonutils.dumps(data)
|
||||
|
||||
|
||||
class XMLDictSerializer(DictSerializer):
|
||||
|
||||
def __init__(self, metadata=None, xmlns=None):
|
||||
"""
|
||||
:param metadata: information needed to deserialize xml into
|
||||
a dictionary.
|
||||
:param xmlns: XML namespace to include with serialized xml
|
||||
"""
|
||||
super(XMLDictSerializer, self).__init__()
|
||||
self.metadata = metadata or {}
|
||||
if not xmlns:
|
||||
xmlns = self.metadata.get('xmlns')
|
||||
if not xmlns:
|
||||
xmlns = constants.XML_NS_V20
|
||||
self.xmlns = xmlns
|
||||
|
||||
def default(self, data):
|
||||
# We expect data to contain a single key which is the XML root or
|
||||
# non root
|
||||
try:
|
||||
key_len = data and len(data.keys()) or 0
|
||||
if (key_len == 1):
|
||||
root_key = data.keys()[0]
|
||||
root_value = data[root_key]
|
||||
else:
|
||||
root_key = constants.VIRTUAL_ROOT_KEY
|
||||
root_value = data
|
||||
doc = etree.Element("_temp_root")
|
||||
used_prefixes = []
|
||||
self._to_xml_node(doc, self.metadata, root_key,
|
||||
root_value, used_prefixes)
|
||||
return self.to_xml_string(list(doc)[0], used_prefixes)
|
||||
except AttributeError as e:
|
||||
LOG.exception(str(e))
|
||||
return ''
|
||||
|
||||
def __call__(self, data):
|
||||
# Provides a migration path to a cleaner WSGI layer, this
|
||||
# "default" stuff and extreme extensibility isn't being used
|
||||
# like originally intended
|
||||
return self.default(data)
|
||||
|
||||
def to_xml_string(self, node, used_prefixes, has_atom=False):
|
||||
self._add_xmlns(node, used_prefixes, has_atom)
|
||||
return etree.tostring(node, encoding='UTF-8')
|
||||
|
||||
#NOTE (ameade): the has_atom should be removed after all of the
|
||||
# xml serializers and view builders have been updated to the current
|
||||
# spec that required all responses include the xmlns:atom, the has_atom
|
||||
# flag is to prevent current tests from breaking
|
||||
def _add_xmlns(self, node, used_prefixes, has_atom=False):
|
||||
node.set('xmlns', self.xmlns)
|
||||
node.set(constants.TYPE_XMLNS, self.xmlns)
|
||||
if has_atom:
|
||||
node.set('xmlns:atom', "http://www.w3.org/2005/Atom")
|
||||
node.set(constants.XSI_NIL_ATTR, constants.XSI_NAMESPACE)
|
||||
ext_ns = self.metadata.get(constants.EXT_NS, {})
|
||||
for prefix in used_prefixes:
|
||||
if prefix in ext_ns:
|
||||
node.set('xmlns:' + prefix, ext_ns[prefix])
|
||||
|
||||
def _to_xml_node(self, parent, metadata, nodename, data, used_prefixes):
|
||||
"""Recursive method to convert data members to XML nodes."""
|
||||
result = etree.SubElement(parent, nodename)
|
||||
if ":" in nodename:
|
||||
used_prefixes.append(nodename.split(":", 1)[0])
|
||||
#TODO(bcwaldon): accomplish this without a type-check
|
||||
if isinstance(data, list):
|
||||
if not data:
|
||||
result.set(
|
||||
constants.TYPE_ATTR,
|
||||
constants.TYPE_LIST)
|
||||
return result
|
||||
singular = metadata.get('plurals', {}).get(nodename, None)
|
||||
if singular is None:
|
||||
if nodename.endswith('s'):
|
||||
singular = nodename[:-1]
|
||||
else:
|
||||
singular = 'item'
|
||||
for item in data:
|
||||
self._to_xml_node(result, metadata, singular, item,
|
||||
used_prefixes)
|
||||
#TODO(bcwaldon): accomplish this without a type-check
|
||||
elif isinstance(data, dict):
|
||||
if not data:
|
||||
result.set(
|
||||
constants.TYPE_ATTR,
|
||||
constants.TYPE_DICT)
|
||||
return result
|
||||
attrs = metadata.get('attributes', {}).get(nodename, {})
|
||||
for k, v in data.items():
|
||||
if k in attrs:
|
||||
result.set(k, str(v))
|
||||
else:
|
||||
self._to_xml_node(result, metadata, k, v,
|
||||
used_prefixes)
|
||||
elif data is None:
|
||||
result.set(constants.XSI_ATTR, 'true')
|
||||
else:
|
||||
if isinstance(data, bool):
|
||||
result.set(
|
||||
constants.TYPE_ATTR,
|
||||
constants.TYPE_BOOL)
|
||||
elif isinstance(data, int):
|
||||
result.set(
|
||||
constants.TYPE_ATTR,
|
||||
constants.TYPE_INT)
|
||||
elif isinstance(data, long):
|
||||
result.set(
|
||||
constants.TYPE_ATTR,
|
||||
constants.TYPE_LONG)
|
||||
elif isinstance(data, float):
|
||||
result.set(
|
||||
constants.TYPE_ATTR,
|
||||
constants.TYPE_FLOAT)
|
||||
LOG.debug(_("Data %(data)s type is %(type)s"),
|
||||
{'data': data,
|
||||
'type': type(data)})
|
||||
result.text = str(data)
|
||||
return result
|
||||
|
||||
def _create_link_nodes(self, xml_doc, links):
|
||||
link_nodes = []
|
||||
for link in links:
|
||||
link_node = xml_doc.createElement('atom:link')
|
||||
link_node.set('rel', link['rel'])
|
||||
link_node.set('href', link['href'])
|
||||
if 'type' in link:
|
||||
link_node.set('type', link['type'])
|
||||
link_nodes.append(link_node)
|
||||
return link_nodes
|
||||
|
||||
|
||||
class TextDeserializer(ActionDispatcher):
|
||||
"""Default request body deserialization"""
|
||||
|
||||
def deserialize(self, datastring, action='default'):
|
||||
return self.dispatch(datastring, action=action)
|
||||
|
||||
def default(self, datastring):
|
||||
return {}
|
||||
|
||||
|
||||
class JSONDeserializer(TextDeserializer):
|
||||
|
||||
def _from_json(self, datastring):
|
||||
try:
|
||||
return jsonutils.loads(datastring)
|
||||
except ValueError:
|
||||
msg = _("Cannot understand JSON")
|
||||
raise exception.MalformedRequestBody(reason=msg)
|
||||
|
||||
def default(self, datastring):
|
||||
return {'body': self._from_json(datastring)}
|
||||
|
||||
|
||||
class XMLDeserializer(TextDeserializer):
|
||||
|
||||
def __init__(self, metadata=None):
|
||||
"""
|
||||
:param metadata: information needed to deserialize xml into
|
||||
a dictionary.
|
||||
"""
|
||||
super(XMLDeserializer, self).__init__()
|
||||
self.metadata = metadata or {}
|
||||
xmlns = self.metadata.get('xmlns')
|
||||
if not xmlns:
|
||||
xmlns = constants.XML_NS_V20
|
||||
self.xmlns = xmlns
|
||||
|
||||
def _get_key(self, tag):
|
||||
tags = tag.split("}", 1)
|
||||
if len(tags) == 2:
|
||||
ns = tags[0][1:]
|
||||
bare_tag = tags[1]
|
||||
ext_ns = self.metadata.get(constants.EXT_NS, {})
|
||||
if ns == self.xmlns:
|
||||
return bare_tag
|
||||
for prefix, _ns in ext_ns.items():
|
||||
if ns == _ns:
|
||||
return prefix + ":" + bare_tag
|
||||
else:
|
||||
return tag
|
||||
|
||||
def _from_xml(self, datastring):
|
||||
if datastring is None:
|
||||
return None
|
||||
plurals = set(self.metadata.get('plurals', {}))
|
||||
try:
|
||||
node = etree.fromstring(datastring)
|
||||
result = self._from_xml_node(node, plurals)
|
||||
root_tag = self._get_key(node.tag)
|
||||
if root_tag == constants.VIRTUAL_ROOT_KEY:
|
||||
return result
|
||||
else:
|
||||
return {root_tag: result}
|
||||
except Exception as e:
|
||||
parseError = False
|
||||
# Python2.7
|
||||
if (hasattr(etree, 'ParseError') and
|
||||
isinstance(e, getattr(etree, 'ParseError'))):
|
||||
parseError = True
|
||||
# Python2.6
|
||||
elif isinstance(e, expat.ExpatError):
|
||||
parseError = True
|
||||
if parseError:
|
||||
msg = _("Cannot understand XML")
|
||||
raise exception.MalformedRequestBody(reason=msg)
|
||||
else:
|
||||
raise
|
||||
|
||||
def _from_xml_node(self, node, listnames):
|
||||
"""Convert a minidom node to a simple Python type.
|
||||
|
||||
:param listnames: list of XML node names whose subnodes should
|
||||
be considered list items.
|
||||
|
||||
"""
|
||||
attrNil = node.get(str(etree.QName(constants.XSI_NAMESPACE, "nil")))
|
||||
attrType = node.get(str(etree.QName(
|
||||
self.metadata.get('xmlns'), "type")))
|
||||
if (attrNil and attrNil.lower() == 'true'):
|
||||
return None
|
||||
elif not len(node) and not node.text:
|
||||
if (attrType and attrType == constants.TYPE_DICT):
|
||||
return {}
|
||||
elif (attrType and attrType == constants.TYPE_LIST):
|
||||
return []
|
||||
else:
|
||||
return ''
|
||||
elif (len(node) == 0 and node.text):
|
||||
converters = {constants.TYPE_BOOL:
|
||||
lambda x: x.lower() == 'true',
|
||||
constants.TYPE_INT:
|
||||
lambda x: int(x),
|
||||
constants.TYPE_LONG:
|
||||
lambda x: long(x),
|
||||
constants.TYPE_FLOAT:
|
||||
lambda x: float(x)}
|
||||
if attrType and attrType in converters:
|
||||
return converters[attrType](node.text)
|
||||
else:
|
||||
return node.text
|
||||
elif self._get_key(node.tag) in listnames:
|
||||
return [self._from_xml_node(n, listnames) for n in node]
|
||||
else:
|
||||
result = dict()
|
||||
for attr in node.keys():
|
||||
if (attr == 'xmlns' or
|
||||
attr.startswith('xmlns:') or
|
||||
attr == constants.XSI_ATTR or
|
||||
attr == constants.TYPE_ATTR):
|
||||
continue
|
||||
result[self._get_key(attr)] = node.get[attr]
|
||||
children = list(node)
|
||||
for child in children:
|
||||
result[self._get_key(child.tag)] = self._from_xml_node(
|
||||
child, listnames)
|
||||
return result
|
||||
|
||||
def find_first_child_named(self, parent, name):
|
||||
"""Search a nodes children for the first child with a given name"""
|
||||
for node in parent.childNodes:
|
||||
if node.nodeName == name:
|
||||
return node
|
||||
return None
|
||||
|
||||
def find_children_named(self, parent, name):
|
||||
"""Return all of a nodes children who have the given name"""
|
||||
for node in parent.childNodes:
|
||||
if node.nodeName == name:
|
||||
yield node
|
||||
|
||||
def extract_text(self, node):
|
||||
"""Get the text field contained by the given node"""
|
||||
if len(node.childNodes) == 1:
|
||||
child = node.childNodes[0]
|
||||
if child.nodeType == child.TEXT_NODE:
|
||||
return child.nodeValue
|
||||
return ""
|
||||
|
||||
def default(self, datastring):
|
||||
return {'body': self._from_xml(datastring)}
|
||||
|
||||
def __call__(self, datastring):
|
||||
# Adding a migration path to allow us to remove unncessary classes
|
||||
return self.default(datastring)
|
||||
|
||||
|
||||
# NOTE(maru): this class is duplicated from quantum.wsgi
|
||||
@ -20,8 +366,8 @@ class Serializer(object):
|
||||
|
||||
def _get_serialize_handler(self, content_type):
|
||||
handlers = {
|
||||
'application/json': self._to_json,
|
||||
'application/xml': self._to_xml,
|
||||
'application/json': JSONDictSerializer(),
|
||||
'application/xml': XMLDictSerializer(self.metadata),
|
||||
}
|
||||
|
||||
try:
|
||||
@ -31,7 +377,7 @@ class Serializer(object):
|
||||
|
||||
def serialize(self, data, content_type):
|
||||
"""Serialize a dictionary into the specified content type."""
|
||||
return self._get_serialize_handler(content_type)(data)
|
||||
return self._get_serialize_handler(content_type).serialize(data)
|
||||
|
||||
def deserialize(self, datastring, content_type):
|
||||
"""Deserialize a string to a dictionary.
|
||||
@ -39,117 +385,16 @@ class Serializer(object):
|
||||
The string must be in the format of a supported MIME type.
|
||||
|
||||
"""
|
||||
try:
|
||||
return self.get_deserialize_handler(content_type)(datastring)
|
||||
except Exception:
|
||||
raise exception.MalformedResponseBody(
|
||||
reason="Unable to deserialize response body")
|
||||
return self.get_deserialize_handler(content_type).deserialize(
|
||||
datastring)
|
||||
|
||||
def get_deserialize_handler(self, content_type):
|
||||
handlers = {
|
||||
'application/json': self._from_json,
|
||||
'application/xml': self._from_xml,
|
||||
'application/json': JSONDeserializer(),
|
||||
'application/xml': XMLDeserializer(self.metadata),
|
||||
}
|
||||
|
||||
try:
|
||||
return handlers[content_type]
|
||||
except Exception:
|
||||
raise exception.InvalidContentType(content_type=content_type)
|
||||
|
||||
def _from_json(self, datastring):
|
||||
return utils.loads(datastring)
|
||||
|
||||
def _from_xml(self, datastring):
|
||||
xmldata = self.metadata.get('application/xml', {})
|
||||
plurals = set(xmldata.get('plurals', {}))
|
||||
node = minidom.parseString(datastring).childNodes[0]
|
||||
return {node.nodeName: self._from_xml_node(node, plurals)}
|
||||
|
||||
def _from_xml_node(self, node, listnames):
|
||||
"""Convert a minidom node to a simple Python type.
|
||||
|
||||
listnames is a collection of names of XML nodes whose subnodes should
|
||||
be considered list items.
|
||||
|
||||
"""
|
||||
if len(node.childNodes) == 1 and node.childNodes[0].nodeType == 3:
|
||||
return node.childNodes[0].nodeValue
|
||||
elif node.nodeName in listnames:
|
||||
return [self._from_xml_node(n, listnames)
|
||||
for n in node.childNodes if n.nodeType != node.TEXT_NODE]
|
||||
else:
|
||||
result = dict()
|
||||
for attr in node.attributes.keys():
|
||||
result[attr] = node.attributes[attr].nodeValue
|
||||
for child in node.childNodes:
|
||||
if child.nodeType != node.TEXT_NODE:
|
||||
result[child.nodeName] = self._from_xml_node(child,
|
||||
listnames)
|
||||
return result
|
||||
|
||||
def _to_json(self, data):
|
||||
return utils.dumps(data)
|
||||
|
||||
def _to_xml(self, data):
|
||||
metadata = self.metadata.get('application/xml', {})
|
||||
# We expect data to contain a single key which is the XML root.
|
||||
root_key = data.keys()[0]
|
||||
doc = minidom.Document()
|
||||
node = self._to_xml_node(doc, metadata, root_key, data[root_key])
|
||||
|
||||
xmlns = node.getAttribute('xmlns')
|
||||
if not xmlns and self.default_xmlns:
|
||||
node.setAttribute('xmlns', self.default_xmlns)
|
||||
|
||||
return node.toprettyxml(indent='', newl='')
|
||||
|
||||
def _to_xml_node(self, doc, metadata, nodename, data):
|
||||
"""Recursive method to convert data members to XML nodes."""
|
||||
result = doc.createElement(nodename)
|
||||
|
||||
# Set the xml namespace if one is specified
|
||||
# TODO(justinsb): We could also use prefixes on the keys
|
||||
xmlns = metadata.get('xmlns', None)
|
||||
if xmlns:
|
||||
result.setAttribute('xmlns', xmlns)
|
||||
if type(data) is list:
|
||||
collections = metadata.get('list_collections', {})
|
||||
if nodename in collections:
|
||||
metadata = collections[nodename]
|
||||
for item in data:
|
||||
node = doc.createElement(metadata['item_name'])
|
||||
node.setAttribute(metadata['item_key'], str(item))
|
||||
result.appendChild(node)
|
||||
return result
|
||||
singular = metadata.get('plurals', {}).get(nodename, None)
|
||||
if singular is None:
|
||||
if nodename.endswith('s'):
|
||||
singular = nodename[:-1]
|
||||
else:
|
||||
singular = 'item'
|
||||
for item in data:
|
||||
node = self._to_xml_node(doc, metadata, singular, item)
|
||||
result.appendChild(node)
|
||||
elif type(data) is dict:
|
||||
collections = metadata.get('dict_collections', {})
|
||||
if nodename in collections:
|
||||
metadata = collections[nodename]
|
||||
for k, v in data.items():
|
||||
node = doc.createElement(metadata['item_name'])
|
||||
node.setAttribute(metadata['item_key'], str(k))
|
||||
text = doc.createTextNode(str(v))
|
||||
node.appendChild(text)
|
||||
result.appendChild(node)
|
||||
return result
|
||||
attrs = metadata.get('attributes', {}).get(nodename, {})
|
||||
for k, v in data.items():
|
||||
if k in attrs:
|
||||
result.setAttribute(k, str(v))
|
||||
else:
|
||||
node = self._to_xml_node(doc, metadata, k, v)
|
||||
result.appendChild(node)
|
||||
else:
|
||||
# Type is atom.
|
||||
node = doc.createTextNode(str(data))
|
||||
result.appendChild(node)
|
||||
return result
|
||||
|
148
quantumclient/openstack/common/jsonutils.py
Normal file
148
quantumclient/openstack/common/jsonutils.py
Normal file
@ -0,0 +1,148 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# Copyright 2011 Justin Santa Barbara
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
'''
|
||||
JSON related utilities.
|
||||
|
||||
This module provides a few things:
|
||||
|
||||
1) A handy function for getting an object down to something that can be
|
||||
JSON serialized. See to_primitive().
|
||||
|
||||
2) Wrappers around loads() and dumps(). The dumps() wrapper will
|
||||
automatically use to_primitive() for you if needed.
|
||||
|
||||
3) This sets up anyjson to use the loads() and dumps() wrappers if anyjson
|
||||
is available.
|
||||
'''
|
||||
|
||||
|
||||
import datetime
|
||||
import inspect
|
||||
import itertools
|
||||
import json
|
||||
import xmlrpclib
|
||||
|
||||
from quantumclient.openstack.common import timeutils
|
||||
|
||||
|
||||
def to_primitive(value, convert_instances=False, level=0):
|
||||
"""Convert a complex object into primitives.
|
||||
|
||||
Handy for JSON serialization. We can optionally handle instances,
|
||||
but since this is a recursive function, we could have cyclical
|
||||
data structures.
|
||||
|
||||
To handle cyclical data structures we could track the actual objects
|
||||
visited in a set, but not all objects are hashable. Instead we just
|
||||
track the depth of the object inspections and don't go too deep.
|
||||
|
||||
Therefore, convert_instances=True is lossy ... be aware.
|
||||
|
||||
"""
|
||||
nasty = [inspect.ismodule, inspect.isclass, inspect.ismethod,
|
||||
inspect.isfunction, inspect.isgeneratorfunction,
|
||||
inspect.isgenerator, inspect.istraceback, inspect.isframe,
|
||||
inspect.iscode, inspect.isbuiltin, inspect.isroutine,
|
||||
inspect.isabstract]
|
||||
for test in nasty:
|
||||
if test(value):
|
||||
return unicode(value)
|
||||
|
||||
# value of itertools.count doesn't get caught by inspects
|
||||
# above and results in infinite loop when list(value) is called.
|
||||
if type(value) == itertools.count:
|
||||
return unicode(value)
|
||||
|
||||
# FIXME(vish): Workaround for LP bug 852095. Without this workaround,
|
||||
# tests that raise an exception in a mocked method that
|
||||
# has a @wrap_exception with a notifier will fail. If
|
||||
# we up the dependency to 0.5.4 (when it is released) we
|
||||
# can remove this workaround.
|
||||
if getattr(value, '__module__', None) == 'mox':
|
||||
return 'mock'
|
||||
|
||||
if level > 3:
|
||||
return '?'
|
||||
|
||||
# The try block may not be necessary after the class check above,
|
||||
# but just in case ...
|
||||
try:
|
||||
# It's not clear why xmlrpclib created their own DateTime type, but
|
||||
# for our purposes, make it a datetime type which is explicitly
|
||||
# handled
|
||||
if isinstance(value, xmlrpclib.DateTime):
|
||||
value = datetime.datetime(*tuple(value.timetuple())[:6])
|
||||
|
||||
if isinstance(value, (list, tuple)):
|
||||
o = []
|
||||
for v in value:
|
||||
o.append(to_primitive(v, convert_instances=convert_instances,
|
||||
level=level))
|
||||
return o
|
||||
elif isinstance(value, dict):
|
||||
o = {}
|
||||
for k, v in value.iteritems():
|
||||
o[k] = to_primitive(v, convert_instances=convert_instances,
|
||||
level=level)
|
||||
return o
|
||||
elif isinstance(value, datetime.datetime):
|
||||
return timeutils.strtime(value)
|
||||
elif hasattr(value, 'iteritems'):
|
||||
return to_primitive(dict(value.iteritems()),
|
||||
convert_instances=convert_instances,
|
||||
level=level + 1)
|
||||
elif hasattr(value, '__iter__'):
|
||||
return to_primitive(list(value),
|
||||
convert_instances=convert_instances,
|
||||
level=level)
|
||||
elif convert_instances and hasattr(value, '__dict__'):
|
||||
# Likely an instance of something. Watch for cycles.
|
||||
# Ignore class member vars.
|
||||
return to_primitive(value.__dict__,
|
||||
convert_instances=convert_instances,
|
||||
level=level + 1)
|
||||
else:
|
||||
return value
|
||||
except TypeError:
|
||||
# Class objects are tricky since they may define something like
|
||||
# __iter__ defined but it isn't callable as list().
|
||||
return unicode(value)
|
||||
|
||||
|
||||
def dumps(value, default=to_primitive, **kwargs):
|
||||
return json.dumps(value, default=default, **kwargs)
|
||||
|
||||
|
||||
def loads(s):
|
||||
return json.loads(s)
|
||||
|
||||
|
||||
def load(s):
|
||||
return json.load(s)
|
||||
|
||||
|
||||
try:
|
||||
import anyjson
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
anyjson._modules.append((__name__, 'dumps', TypeError,
|
||||
'loads', ValueError, 'load'))
|
||||
anyjson.force_implementation(__name__)
|
164
quantumclient/openstack/common/timeutils.py
Normal file
164
quantumclient/openstack/common/timeutils.py
Normal file
@ -0,0 +1,164 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Time related utilities and helper functions.
|
||||
"""
|
||||
|
||||
import calendar
|
||||
import datetime
|
||||
|
||||
import iso8601
|
||||
|
||||
|
||||
TIME_FORMAT = "%Y-%m-%dT%H:%M:%S"
|
||||
PERFECT_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%f"
|
||||
|
||||
|
||||
def isotime(at=None):
|
||||
"""Stringify time in ISO 8601 format"""
|
||||
if not at:
|
||||
at = utcnow()
|
||||
str = at.strftime(TIME_FORMAT)
|
||||
tz = at.tzinfo.tzname(None) if at.tzinfo else 'UTC'
|
||||
str += ('Z' if tz == 'UTC' else tz)
|
||||
return str
|
||||
|
||||
|
||||
def parse_isotime(timestr):
|
||||
"""Parse time from ISO 8601 format"""
|
||||
try:
|
||||
return iso8601.parse_date(timestr)
|
||||
except iso8601.ParseError as e:
|
||||
raise ValueError(e.message)
|
||||
except TypeError as e:
|
||||
raise ValueError(e.message)
|
||||
|
||||
|
||||
def strtime(at=None, fmt=PERFECT_TIME_FORMAT):
|
||||
"""Returns formatted utcnow."""
|
||||
if not at:
|
||||
at = utcnow()
|
||||
return at.strftime(fmt)
|
||||
|
||||
|
||||
def parse_strtime(timestr, fmt=PERFECT_TIME_FORMAT):
|
||||
"""Turn a formatted time back into a datetime."""
|
||||
return datetime.datetime.strptime(timestr, fmt)
|
||||
|
||||
|
||||
def normalize_time(timestamp):
|
||||
"""Normalize time in arbitrary timezone to UTC naive object"""
|
||||
offset = timestamp.utcoffset()
|
||||
if offset is None:
|
||||
return timestamp
|
||||
return timestamp.replace(tzinfo=None) - offset
|
||||
|
||||
|
||||
def is_older_than(before, seconds):
|
||||
"""Return True if before is older than seconds."""
|
||||
if isinstance(before, basestring):
|
||||
before = parse_strtime(before).replace(tzinfo=None)
|
||||
return utcnow() - before > datetime.timedelta(seconds=seconds)
|
||||
|
||||
|
||||
def is_newer_than(after, seconds):
|
||||
"""Return True if after is newer than seconds."""
|
||||
if isinstance(after, basestring):
|
||||
after = parse_strtime(after).replace(tzinfo=None)
|
||||
return after - utcnow() > datetime.timedelta(seconds=seconds)
|
||||
|
||||
|
||||
def utcnow_ts():
|
||||
"""Timestamp version of our utcnow function."""
|
||||
return calendar.timegm(utcnow().timetuple())
|
||||
|
||||
|
||||
def utcnow():
|
||||
"""Overridable version of utils.utcnow."""
|
||||
if utcnow.override_time:
|
||||
try:
|
||||
return utcnow.override_time.pop(0)
|
||||
except AttributeError:
|
||||
return utcnow.override_time
|
||||
return datetime.datetime.utcnow()
|
||||
|
||||
|
||||
utcnow.override_time = None
|
||||
|
||||
|
||||
def set_time_override(override_time=datetime.datetime.utcnow()):
|
||||
"""
|
||||
Override utils.utcnow to return a constant time or a list thereof,
|
||||
one at a time.
|
||||
"""
|
||||
utcnow.override_time = override_time
|
||||
|
||||
|
||||
def advance_time_delta(timedelta):
|
||||
"""Advance overridden time using a datetime.timedelta."""
|
||||
assert(not utcnow.override_time is None)
|
||||
try:
|
||||
for dt in utcnow.override_time:
|
||||
dt += timedelta
|
||||
except TypeError:
|
||||
utcnow.override_time += timedelta
|
||||
|
||||
|
||||
def advance_time_seconds(seconds):
|
||||
"""Advance overridden time by seconds."""
|
||||
advance_time_delta(datetime.timedelta(0, seconds))
|
||||
|
||||
|
||||
def clear_time_override():
|
||||
"""Remove the overridden time."""
|
||||
utcnow.override_time = None
|
||||
|
||||
|
||||
def marshall_now(now=None):
|
||||
"""Make an rpc-safe datetime with microseconds.
|
||||
|
||||
Note: tzinfo is stripped, but not required for relative times."""
|
||||
if not now:
|
||||
now = utcnow()
|
||||
return dict(day=now.day, month=now.month, year=now.year, hour=now.hour,
|
||||
minute=now.minute, second=now.second,
|
||||
microsecond=now.microsecond)
|
||||
|
||||
|
||||
def unmarshall_time(tyme):
|
||||
"""Unmarshall a datetime dict."""
|
||||
return datetime.datetime(day=tyme['day'],
|
||||
month=tyme['month'],
|
||||
year=tyme['year'],
|
||||
hour=tyme['hour'],
|
||||
minute=tyme['minute'],
|
||||
second=tyme['second'],
|
||||
microsecond=tyme['microsecond'])
|
||||
|
||||
|
||||
def delta_seconds(before, after):
|
||||
"""
|
||||
Compute the difference in seconds between two date, time, or
|
||||
datetime objects (as a float, to microsecond resolution).
|
||||
"""
|
||||
delta = after - before
|
||||
try:
|
||||
return delta.total_seconds()
|
||||
except AttributeError:
|
||||
return ((delta.days * 24 * 3600) + delta.seconds +
|
||||
float(delta.microseconds) / (10 ** 6))
|
@ -22,8 +22,9 @@ import urllib
|
||||
|
||||
from quantumclient.client import HTTPClient
|
||||
from quantumclient.common import _
|
||||
from quantumclient.common import constants
|
||||
from quantumclient.common import exceptions
|
||||
from quantumclient.common.serializer import Serializer
|
||||
from quantumclient.common import serializer
|
||||
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
@ -139,18 +140,6 @@ class Client(object):
|
||||
|
||||
"""
|
||||
|
||||
#Metadata for deserializing xml
|
||||
_serialization_metadata = {
|
||||
"application/xml": {
|
||||
"attributes": {
|
||||
"network": ["id", "name"],
|
||||
"port": ["id", "mac_address"],
|
||||
"subnet": ["id", "prefix"]},
|
||||
"plurals": {
|
||||
"networks": "network",
|
||||
"ports": "port",
|
||||
"subnets": "subnet", }, }, }
|
||||
|
||||
networks_path = "/networks"
|
||||
network_path = "/networks/%s"
|
||||
ports_path = "/ports"
|
||||
@ -182,6 +171,33 @@ class Client(object):
|
||||
disassociate_pool_health_monitors_path = (
|
||||
"/lb/pools/%(pool)s/health_monitors/%(health_monitor)s")
|
||||
|
||||
# API has no way to report plurals, so we have to hard code them
|
||||
EXTED_PLURALS = {'routers': 'router',
|
||||
'floatingips': 'floatingip',
|
||||
'service_types': 'service_type',
|
||||
'service_definitions': 'service_definition',
|
||||
'security_groups': 'security_group',
|
||||
'security_group_rules': 'security_group_rule',
|
||||
'vips': 'vip',
|
||||
'pools': 'pool',
|
||||
'members': 'member',
|
||||
'health_monitors': 'health_monitor',
|
||||
'quotas': 'quota',
|
||||
}
|
||||
|
||||
def get_attr_metadata(self):
|
||||
if self.format == 'json':
|
||||
return {}
|
||||
old_request_format = self.format
|
||||
self.format = 'json'
|
||||
exts = self.list_extensions()['extensions']
|
||||
self.format = old_request_format
|
||||
ns = dict([(ext['alias'], ext['namespace']) for ext in exts])
|
||||
self.EXTED_PLURALS.update(constants.PLURALS)
|
||||
return {'plurals': self.EXTED_PLURALS,
|
||||
'xmlns': constants.XML_NS_V20,
|
||||
constants.EXT_NS: ns}
|
||||
|
||||
@APIParamsCall
|
||||
def get_quotas_tenant(self, **_params):
|
||||
"""Fetch tenant info in server's context for
|
||||
@ -669,16 +685,14 @@ class Client(object):
|
||||
|
||||
def _handle_fault_response(self, status_code, response_body):
|
||||
# Create exception with HTTP status code and message
|
||||
error_message = response_body
|
||||
_logger.debug("Error message: %s", error_message)
|
||||
_logger.debug("Error message: %s", response_body)
|
||||
# Add deserialized error message to exception arguments
|
||||
try:
|
||||
des_error_body = Serializer().deserialize(error_message,
|
||||
self.content_type())
|
||||
des_error_body = self.deserialize(response_body, status_code)
|
||||
except:
|
||||
# If unable to deserialized body it is probably not a
|
||||
# Quantum error
|
||||
des_error_body = {'message': error_message}
|
||||
des_error_body = {'message': response_body}
|
||||
# Raise the appropriate exception
|
||||
exception_handler_v20(status_code, des_error_body)
|
||||
|
||||
@ -719,7 +733,8 @@ class Client(object):
|
||||
if data is None:
|
||||
return None
|
||||
elif type(data) is dict:
|
||||
return Serializer().serialize(data, self.content_type())
|
||||
return serializer.Serializer(
|
||||
self.get_attr_metadata()).serialize(data, self.content_type())
|
||||
else:
|
||||
raise Exception("unable to serialize object of type = '%s'" %
|
||||
type(data))
|
||||
@ -730,17 +745,16 @@ class Client(object):
|
||||
"""
|
||||
if status_code == 204:
|
||||
return data
|
||||
return Serializer(self._serialization_metadata).deserialize(
|
||||
data, self.content_type())
|
||||
return serializer.Serializer(self.get_attr_metadata()).deserialize(
|
||||
data, self.content_type())['body']
|
||||
|
||||
def content_type(self, format=None):
|
||||
def content_type(self, _format=None):
|
||||
"""
|
||||
Returns the mime-type for either 'xml' or 'json'. Defaults to the
|
||||
currently set format
|
||||
"""
|
||||
if not format:
|
||||
format = self.format
|
||||
return "application/%s" % (format)
|
||||
_format = _format or self.format
|
||||
return "application/%s" % (_format)
|
||||
|
||||
def retry_request(self, method, action, body=None,
|
||||
headers=None, params=None):
|
||||
|
@ -1,6 +1,7 @@
|
||||
cliff>=1.2.1
|
||||
argparse
|
||||
cliff>=1.2.1
|
||||
httplib2
|
||||
iso8601
|
||||
prettytable>=0.6.0
|
||||
simplejson
|
||||
pyparsing>=1.5.6,<2.0
|
||||
simplejson
|
||||
|
2
tox.ini
2
tox.ini
@ -11,7 +11,7 @@ deps = -r{toxinidir}/tools/test-requires
|
||||
commands = python setup.py testr --testr-args='{posargs}'
|
||||
|
||||
[testenv:pep8]
|
||||
commands = pep8 --repeat --show-source --exclude=.venv,.tox,dist,doc .
|
||||
commands = pep8 --repeat --show-source --ignore=E125 --exclude=.venv,.tox,dist,doc .
|
||||
|
||||
[testenv:venv]
|
||||
commands = {posargs}
|
||||
|
Loading…
x
Reference in New Issue
Block a user