Example #1
0
 def __init__(self, access_key, secret_key, endpoint = None):
     """ Initialize client for given query endpoint.
     None as endpoint means default endpoint.
     """
     self.access_key = access_key
     self.kws = KWSSigner(secret_key)
     api_path = '/api'
     if endpoint is not None:
         self.api_url = endpoint+api_path
     else:
         self.api_url = QUERY_ENDPOINT+api_path
     self.debugging = False
Example #2
0
class BasicDataUploadClient(object):
    """ Client for kooaba Data Upload API. """

    def __init__(self, access_key, secret_key, endpoint = None):
        """ Initialize client for given query endpoint.
        None as endpoint means default endpoint.
        """
        self.access_key = access_key
        self.kws = KWSSigner(secret_key)
        api_path = '/api'
        if endpoint is not None:
            self.api_url = endpoint+api_path
        else:
            self.api_url = QUERY_ENDPOINT+api_path
        self.debugging = False

    def activate_image(self, image_id):
        """ Activate the image.
        Returns current status of the image.
        """
        return self._set_image_status(image_id, 'ACTIVE')

    def add_resource_file(self, item_id, title, section, filename):
        """ Add a file resource to an item.
        Raises exception on error.
        """
        upload_id = self.upload_from_file(filename)
        xml = self._generate_basic_resource_xml(title, section)
        file_elem = ET.SubElement(xml, "file")
        self._add_xml_subelement(file_elem, "upload_id", upload_id)
        (_response, _body) = self._post_data("/items/%s/item_resources.xml" % item_id, self._serialize_xml(xml), 'application/xml')

    def add_resource_uri(self, item_id, title, section, uri, locale=None):
        """ Add a URI resource to an item.
        Raises exception on error.
        """
        xml = self._generate_basic_resource_xml(title, section)
        self._add_xml_subelement(xml, "uri", uri)
        if locale:
            self._add_xml_subelement(xml, "locale", locale)
        (_response, _body) = self._post_data("/items/%s/item_resources.xml" % item_id, self._serialize_xml(xml), 'application/xml')

    def change_item_medium_type(self, item_id, medium_type, title, metadata):
        """ Change the medium type of the item and upload new metadata.
        The title is a required metadata title, in general identical to the
        item title. The metadata is a dictionary with the metadata entries.
        When medium_type is None, the curent medium type is removed from the
        item.
        Raises exception on error.
        """
        xml = ET.Element("medium")
        if (medium_type is not None) and (medium_type != ""):
            self._add_xml_subelement(xml, "type", 'Medium::'+medium_type)
            self._add_xml_subelement(xml, 'title', title)
            for (key, value) in metadata.items():
                self._add_xml_subelement(xml, key, value)
        (_response, _body) = self._put_data("/items/%s/medium.xml" % item_id, self._serialize_xml(xml), 'application/xml')

    def create_image_from_upload(self, item_id, upload_id):
        """ Associate uploaded image with an item.
        Returns image ID.
        """
        xml = ET.Element("image")
        file_elem = ET.SubElement(xml, "file")
        self._add_xml_subelement(file_elem, "upload-id", upload_id)
        (_response, body) = self._post_data("/items/%s/images.xml" % item_id, self._serialize_xml(xml), 'application/xml')
        return self._id_from_xml_string(body)

    def create_item(self, group_id, title, extras=None):
        """ Create a new item with title in the group with group_id.
        Optional metadata entries (external_id, reference_id, locale) are
        specified as dictionary in extras (any entries with value None are
        ignored).
        Returns ID of the created item.
        Raises exception:
            - RuntimeError: API call failed with an error.
        """
        xml = ET.Element("item")
        self._add_xml_subelement(xml, "title", title)
        if extras is None:
            extras = {}
        for (key, value) in extras.items():
            if value is not None:
                self._add_xml_subelement(xml, key, value)
        (_response, body) = self._post_data("/groups/%s/items.xml" % group_id, self._serialize_xml(xml), 'application/xml')
        return self._id_from_xml_string(body)

    def deactivate_image(self, image_id):
        """ Deactivate the image.
        Returns current status of the image.
        """
        return self._set_image_status(image_id, 'INACTIVE')

    def delete_image(self, image_id):
        """ Delete the image.
        Raises exception on error.
        """
        (_response, _body) = self._send_request('DELETE', "/images/%s.xml" % image_id)

    def delete_item(self, item_id):
        """ Delete the item.
        Raises exception on error.
        """
        (_response, _body) = self._send_request('DELETE', "/items/%s.xml" % item_id)

    def get_group(self, group_id):
        """ Return description of the group. """
        (_response, body) = self._send_request('GET', "/groups/%s.xml" % group_id)
        return body

    def get_group_items(self, group_id):
        """ Return items from the group. """
        (_response, body) = self._send_request('GET', "/groups/%s/items.xml" % group_id)
        return body

    def get_image(self, image_id):
        """ Return description of the image. """
        (_response, body) = self._send_request('GET', "/images/%s.xml" % image_id)
        return body

    def get_image_status(self, image_id):
        """ Return current image status. """
        (_response, body) = self._send_request('GET', "/images/%s.xml" % image_id)
        return self._status_from_xml_string(body)

    def get_item(self, item_id):
        """ Return description of the item. """
        (_response, body) = self._send_request('GET', "/items/%s.xml" % item_id)
        return body

    def get_item_resources(self, item_id):
        """ Return resources of the item. """
        (_response, body) = self._send_request('GET', "/items/%s/item_resources.xml" % item_id)
        return body

    def set_debug(self, flag):
        """ Enable/disable debugging printouts according to the flag. """
        self.debugging = flag

    def update_item(self, item_id, extras = None):
        """ Update the item according to metadata in extras.
        The metadata entries (title, external_id, reference_id, locale) are
        specified as dictionary (any entries with value None are ignored).
        Raises exception:
            - RuntimeError: API call failed with an error.
        """
        if extras is None:
            extras = {}
        xml = ET.Element("item")
        for (key, value) in extras.items():
            if value is not None:
                self._add_xml_subelement(xml, key, value)
        (_response, _body) = self._put_data("/items/%s.xml" % item_id, self._serialize_xml(xml), 'application/xml')

    def upload_data(self, data, content_type):
        """ Upload data from memory.
        Returns upload ID.
        """
        (_response, body) = self._post_data('/uploads.xml', data, content_type)
        return self._id_from_xml_string(body)

    def upload_from_file(self, filename, content_type = None):
        """ Upload a file.
        Returns upload ID.
        """
        if content_type is None:
            (content_type, _encoding) = mimetypes.guess_type(filename)
        with open(filename, 'rb') as f:
            return self.upload_data(f.read(), content_type)

    def _add_xml_subelement(self, root, name, text):
        """ Add a text sub-element to root. """
        elem = ET.SubElement(root, name)
        elem.text = unicode(text, "UTF-8")

    def _element_from_xml(self, xml, name):
        """ Extract element name from supplied XML string.
        Returns the element text content as string.
        Raises KeyError if there is no such element.
        """
        elem = xml.find(name)
        if elem is None:
            raise KeyError("No '"+name+"' element in the supplied XML: "+ET.tostring(xml))
        return elem.text

    def _generate_basic_resource_xml(self, title, section):
        """ Return resource upload XML fragment with title and section. """
        xml = ET.Element("resource")
        self._add_xml_subelement(xml, "title", title)
        self._add_xml_subelement(xml, "section", section)
        return xml

    def _id_from_xml(self, xml):
        """ Extract id element from supplied XML string.
        Returns the ID as string.
        Raises KeyError if there is no ID element.
        """
        return self._element_from_xml(xml, "id")

    def _id_from_xml_string(self, xml_string):
        """ Extract id element from supplied XML string.
        Returns the ID as string.
        Raises KeyError if there is no ID element.
        """
        xml = ET.fromstring(xml_string)
        return self._id_from_xml(xml)

    def _post_data(self, api_path, data, content_type):
        """ Post data to an API node specified by api_path.
        See _send_request() for further details.
        """
        return self._send_request('POST', api_path, data, content_type)

    def _put_data(self, api_path, data, content_type):
        """ Put data to an API node specified by api_path.
        See _send_request() for further details.
        """
        return self._send_request('PUT', api_path, data, content_type)

    def _send_request(self, method, api_path, data=None, content_type=None):
        """ Send (POST/PUT/GET/DELETE according to the method) data to an API
        node specified by api_path.

        Returns tuple (response, body) as returned by the API call. The
        response is a HttpResponse object describint HTTP headers and status
        line.

        Raises exception on error:
            - IOError: Failure performing HTTP call
            - RuntimeError: Unsupported transport scheme.
            - RuntimeError: API call returned an error.
        """
        if self.debugging:
            if data is None:
                print "%s ...%s" % (method, api_path)
            elif len(data) < 4096:
                print "%s ...%s:\n%s" % (method, api_path, data)
            else:
                print "%s ...%s: %sB" % (method, api_path, len(data))
        if '://' not in self.api_url:
            # endpoint as a host or host:port
            parsed_url = urlparse('http://'+self.api_url+api_path)
        else:
            parsed_url = urlparse(self.api_url+api_path)
        if (parsed_url.scheme != 'http') and (parsed_url.scheme != 'https'):
            raise RuntimeError("URL scheme '%s' not supported" % parsed_url.scheme)
        port = parsed_url.port
        if port is None:
            port = 80
        host = parsed_url.hostname
        http = httplib.HTTPConnection(host, port)
        try:
            date = email.utils.formatdate(None, localtime=False, usegmt=True)
            if data is not None:
                signature = self.kws.sign_with_content(method, data, content_type, date, parsed_url.path)
            else:
                signature = self.kws.sign_with_no_content(method, content_type, date, parsed_url.path)
            headers = {
                    'Authorization': 'KWS %s:%s' % (self.access_key, signature),
                    'Date': date
                    }
            if content_type is not None:
                headers['Content-Type'] = content_type
            if data is not None:
                headers['Content-Length'] = str(len(data))
            try:
                http.request(method, parsed_url.path, headers=headers, body=data)
            except Exception, e:
                raise IOError("Error during request: %s: %s" % (type(e), e))
            response = http.getresponse()
            # we have to read the response before the http connection is closed
            body = response.read()
            if self.debugging:
                print "HTTP response status:", response.status, response.reason
                print "Body:"
                print body
            if (response.status < 200) or (response.status > 299):
                raise RuntimeError("API call returned status %s %s. Message: %s" % (response.status, response.reason, body))
            return (response, body)
        finally: